Skip to content

Commit

Permalink
Merge pull request #72 from Quramy/remove-duplicated-fragments
Browse files Browse the repository at this point in the history
Remove duplicated fragments
  • Loading branch information
Quramy authored Jan 8, 2020
2 parents dbf3f31 + 2672734 commit 1655c27
Show file tree
Hide file tree
Showing 18 changed files with 362 additions and 10 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,31 @@ extend type Query {
]
```

### `removeDuplicatedFragments`

It's optional and default: `true`. By default, this plugin ignores duplicated fragment definitions such as:

```ts
const fragment = gql`
fragment A on Query {
id
}
`;

const query = gql`
${fragment}
query MyQuery {
...A
}
${fragment}
# duplicated fragment definiton
`;
```

This option affects all editor supporting functions and result of CLI commands.

If you set this option `false`, this plugin passes through query document without removing duplication.

## Template strings

This tool analyzes template string literals in .ts files such as:
Expand Down
30 changes: 30 additions & 0 deletions src/analyzer/__snapshots__/extractor.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,33 @@ Array [
",
]
`;

exports[`Extractor should extract GraphQL documents and shrink duplicated fragments when removeDuplicatedFragments: false 1`] = `
Array [
"fragment A on Query {
hello
}
fragment A on Query {
hello
}
query MyQuery {
...A
}
",
]
`;

exports[`Extractor should extract GraphQL documents and shrink duplicated fragments when removeDuplicatedFragments: true 1`] = `
Array [
"fragment A on Query {
hello
}
query MyQuery {
...A
}
",
]
`;
1 change: 1 addition & 0 deletions src/analyzer/analyzer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function createTestingAnalyzer({ files: sourceFiles, sdl }: CreateTestingAnalyze
name: 'ts-graphql-plugin',
schema: '/schema.graphql',
localSchemaExtensions: [],
removeDuplicatedFragments: true,
tag: 'gql',
};
const schemaManagerHost = createTestingSchemaManagerHost({
Expand Down
6 changes: 5 additions & 1 deletion src/analyzer/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ export class Analyzer {
languageService: langService,
languageServiceHost: this._languageServiceHost,
});
this._extractor = new Extractor({ scriptSourceHelper: this._scriptSourceHelper, debug: this._debug });
this._extractor = new Extractor({
removeDuplicatedFragments: this._pluginConfig.removeDuplicatedFragments === false ? false : true,
scriptSourceHelper: this._scriptSourceHelper,
debug: this._debug,
});
}

extract() {
Expand Down
57 changes: 56 additions & 1 deletion src/analyzer/extractor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { Extractor, ExtractSucceededResult } from './extractor';
import { createTestingLanguageServiceAndHost } from '../ts-ast-util/testing/testing-language-service';
import { createScriptSourceHelper } from '../ts-ast-util/script-source-helper';

function createExtractor(files: { fileName: string; content: string }[]) {
function createExtractor(files: { fileName: string; content: string }[], removeDuplicatedFragments = false) {
const { languageService, languageServiceHost } = createTestingLanguageServiceAndHost({ files });
const extractor = new Extractor({
removeDuplicatedFragments,
scriptSourceHelper: createScriptSourceHelper({ languageService, languageServiceHost }),
debug: () => {},
});
Expand Down Expand Up @@ -38,6 +39,60 @@ describe(Extractor, () => {
expect(result.map(r => print(r.documentNode!))).toMatchSnapshot();
});

it('should extract GraphQL documents and shrink duplicated fragments when removeDuplicatedFragments: true', () => {
const extractor = createExtractor(
[
{
fileName: 'main.ts',
content: `
import gql from 'graphql-tag';
const query = gql\`
fragment A on Query {
hello
}
fragment A on Query {
hello
}
query MyQuery {
...A
}
\`;
`,
},
],
true,
);
const result = extractor.extract(['main.ts'], 'gql');
expect(result.map(r => print(r.documentNode!))).toMatchSnapshot();
});

it('should extract GraphQL documents and shrink duplicated fragments when removeDuplicatedFragments: false', () => {
const extractor = createExtractor(
[
{
fileName: 'main.ts',
content: `
import gql from 'graphql-tag';
const query = gql\`
fragment A on Query {
hello
}
fragment A on Query {
hello
}
query MyQuery {
...A
}
\`;
`,
},
],
false,
);
const result = extractor.extract(['main.ts'], 'gql');
expect(result.map(r => print(r.documentNode!))).toMatchSnapshot();
});

it('should store GraphQL syntax errors with invalid document', () => {
const extractor = createExtractor([
{
Expand Down
33 changes: 31 additions & 2 deletions src/analyzer/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { visit } from 'graphql/language';
import { isTagged, ScriptSourceHelper, ResolvedTemplateInfo } from '../ts-ast-util';
import { ManifestOutput, ManifestDocumentEntry, OperationType } from './types';
import { ErrorWithLocation } from '../errors';
import { detectDuplicatedFragments } from '../gql-ast-util';

export type ExtractorOptions = {
removeDuplicatedFragments: boolean;
scriptSourceHelper: ScriptSourceHelper;
debug: (msg: string) => void;
};
Expand Down Expand Up @@ -40,10 +42,12 @@ export type ExtractSucceededResult = {
export type ExtractResult = ExtractTemplateResolveErrorResult | ExtractGraphQLErrorResult | ExtractSucceededResult;

export class Extractor {
private readonly _removeDuplicatedFragments: boolean;
private readonly _helper: ScriptSourceHelper;
private readonly _debug: (msg: string) => void;

constructor({ debug, scriptSourceHelper }: ExtractorOptions) {
constructor({ debug, removeDuplicatedFragments, scriptSourceHelper }: ExtractorOptions) {
this._removeDuplicatedFragments = removeDuplicatedFragments;
this._helper = scriptSourceHelper;
this._debug = debug;
}
Expand Down Expand Up @@ -83,10 +87,35 @@ export class Extractor {
return results.map(result => {
if (!result.resolevedTemplateInfo) return result;
try {
const documentNode = parse(result.resolevedTemplateInfo.combinedText);
const rawDocumentNode = parse(result.resolevedTemplateInfo.combinedText);
if (!this._removeDuplicatedFragments) {
return {
...result,
documentNode: rawDocumentNode,
};
}
const duplicatedInfo = detectDuplicatedFragments(rawDocumentNode);
const updatedResolvedInfo = duplicatedInfo.reduce(
(acc, fragmentInfo) => this._helper.updateTemplateLiteralInfo(acc, fragmentInfo),
result.resolevedTemplateInfo,
);
const duplicatedFragmentUsedMap = duplicatedInfo.reduce(
(acc, fragmentInfo) => ({ ...acc, [fragmentInfo.name]: false }),
{} as { [name: string]: boolean },
);
const documentNode = visit(rawDocumentNode, {
FragmentDefinition: node => {
if (duplicatedFragmentUsedMap[node.name.value] === false) {
duplicatedFragmentUsedMap[node.name.value] = true;
} else if (duplicatedFragmentUsedMap[node.name.value]) {
return null;
}
},
});
return {
...result,
documentNode,
resolevedTemplateInfo: updatedResolvedInfo,
};
} catch (error) {
return {
Expand Down
1 change: 1 addition & 0 deletions src/analyzer/markdown-reporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createScriptSourceHelper } from '../ts-ast-util/script-source-helper';
function createExtractor(files: { fileName: string; content: string }[]) {
const { languageService, languageServiceHost } = createTestingLanguageServiceAndHost({ files });
const extractor = new Extractor({
removeDuplicatedFragments: true,
scriptSourceHelper: createScriptSourceHelper({ languageService, languageServiceHost }),
debug: () => {},
});
Expand Down
11 changes: 11 additions & 0 deletions src/gql-ast-util/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`detectDuplicatedFragments should detect duplicated fragments info 1`] = `
Array [
Object {
"end": 149,
"name": "Hoge",
"start": 106,
},
]
`;
31 changes: 31 additions & 0 deletions src/gql-ast-util/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { detectDuplicatedFragments } from './';
import { parse } from 'graphql';

describe(detectDuplicatedFragments, () => {
it('should detect duplicated fragments info', () => {
const documentContent = `
fragment Hoge on Query {
id
}
fragment Foo on Query {
id
}
fragment Hoge on Query {
id
}
`;
expect(detectDuplicatedFragments(parse(documentContent))).toMatchSnapshot();
});

it('should return empty array when no duplication', () => {
const documentContent = `
fragment Hoge on Query {
id
}
fragment Foo on Query {
id
}
`;
expect(detectDuplicatedFragments(parse(documentContent))).toStrictEqual([]);
});
});
22 changes: 22 additions & 0 deletions src/gql-ast-util/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DocumentNode, FragmentDefinitionNode } from 'graphql';

export function detectDuplicatedFragments(documentNode: DocumentNode) {
const fragments: FragmentDefinitionNode[] = [];
const duplicatedFragments: FragmentDefinitionNode[] = [];
documentNode.definitions.forEach(def => {
if (def.kind === 'FragmentDefinition') {
if (fragments.some(f => f.name.value === def.name.value)) {
duplicatedFragments.push(def);
} else {
fragments.push(def);
}
}
});
return duplicatedFragments.map(def => {
return {
name: def.name.value,
start: def.loc!.start,
end: def.loc!.end,
};
});
}
31 changes: 26 additions & 5 deletions src/graphql-language-service-adapter/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import ts from 'typescript';
import { GraphQLSchema } from 'graphql';
import { GraphQLSchema, parse } from 'graphql';
import { CompletionItem, Diagnostic, Position } from 'graphql-language-service-types';
import { getAutocompleteSuggestions, getDiagnostics, getHoverInformation } from 'graphql-language-service-interface';

import { isTagged, TagCondition } from '../ts-ast-util';
import { SchemaBuildErrorInfo } from '../schema-manager/schema-manager';
import { ScriptSourceHelper } from '../ts-ast-util/types';
import { detectDuplicatedFragments } from '../gql-ast-util';

export interface GraphQLLanguageServiceAdapterCreateOptions {
schema?: GraphQLSchema | null;
schemaErrors?: SchemaBuildErrorInfo[] | null;
logger?: (msg: string) => void;
tag?: string;
removeDuplicatedFragments: boolean;
}

type GetCompletionAtPosition = ts.LanguageService['getCompletionsAtPosition'];
Expand Down Expand Up @@ -93,12 +95,14 @@ export class GraphQLLanguageServiceAdapter {
private _schemaErrors?: SchemaBuildErrorInfo[] | null;
private _schema?: GraphQLSchema | null;
private _tagCondition?: TagCondition;
private _removeDuplicatedFragments: boolean;

constructor(private _helper: ScriptSourceHelper, opt: GraphQLLanguageServiceAdapterCreateOptions = {}) {
constructor(private _helper: ScriptSourceHelper, opt: GraphQLLanguageServiceAdapterCreateOptions) {
if (opt.logger) this._logger = opt.logger;
if (opt.schemaErrors) this.updateSchema(opt.schemaErrors, null);
if (opt.schema) this.updateSchema(null, opt.schema);
if (opt.tag) this._tagCondition = opt.tag;
this._removeDuplicatedFragments = opt.removeDuplicatedFragments;
}

updateSchema(errors: SchemaBuildErrorInfo[] | null, schema: GraphQLSchema | null) {
Expand All @@ -120,7 +124,7 @@ export class GraphQLLanguageServiceAdapter {
if (!this._schema) return delegate(fileName, position, options);
const node = this._findTemplateNode(fileName, position);
if (!node) return delegate(fileName, position, options);
const resolvedTemplateInfo = this._helper.resolveTemplateLiteral(fileName, node);
const resolvedTemplateInfo = this._resolveTemplateInfo(fileName, node);
if (!resolvedTemplateInfo) {
return delegate(fileName, position, options);
}
Expand Down Expand Up @@ -166,7 +170,7 @@ export class GraphQLLanguageServiceAdapter {
});
} else if (this._schema) {
const diagnosticsAndResolvedInfoList = nodes.map(n => {
const resolvedTemplateInfo = this._helper.resolveTemplateLiteral(fileName, n);
const resolvedTemplateInfo = this._resolveTemplateInfo(fileName, n);
if (!resolvedTemplateInfo) return;
return {
resolvedTemplateInfo,
Expand Down Expand Up @@ -224,7 +228,7 @@ export class GraphQLLanguageServiceAdapter {
if (!this._schema) return delegate(fileName, position);
const node = this._findTemplateNode(fileName, position);
if (!node) return delegate(fileName, position);
const resolvedTemplateInfo = this._helper.resolveTemplateLiteral(fileName, node);
const resolvedTemplateInfo = this._resolveTemplateInfo(fileName, node);
if (!resolvedTemplateInfo) return delegate(fileName, position);
const { combinedText, getInnerPosition, convertInnerPosition2InnerLocation } = resolvedTemplateInfo;
const cursor = new SimplePosition(convertInnerPosition2InnerLocation(getInnerPosition(position).pos + 1));
Expand Down Expand Up @@ -260,5 +264,22 @@ export class GraphQLLanguageServiceAdapter {
return node;
}

private _resolveTemplateInfo(fileName: string, node: ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral) {
const originalInfo = this._helper.resolveTemplateLiteral(fileName, node);
if (!originalInfo) return;
if (!this._removeDuplicatedFragments) return originalInfo;
try {
const documentNode = parse(originalInfo.combinedText);
const duplicatedFragmentInfoList = detectDuplicatedFragments(documentNode);
const info = duplicatedFragmentInfoList.reduce((acc, fragmentInfo) => {
return this._helper.updateTemplateLiteralInfo(acc, fragmentInfo);
}, originalInfo);
return info;
} catch (error) {
return originalInfo;
}
return originalInfo;
}

private _logger: (msg: string) => void = () => {};
}
Loading

0 comments on commit 1655c27

Please sign in to comment.