From 3926c9dfc4a2d84bd9ca9e3837d2016461e38f99 Mon Sep 17 00:00:00 2001 From: Federico Zivolo Date: Fri, 17 May 2019 14:16:33 +0200 Subject: [PATCH 1/2] fix: dedupe fragments (fix #90) --- package.json | 1 + .../__snapshots__/macro.test.js.snap | 150 +++++++++++++++++- src/__tests__/macro.test.js | 9 ++ src/utils/compileWithFragment.js | 19 ++- yarn.lock | 23 +++ 5 files changed, 199 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index de83ab2..7ca8c05 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "changelog": "conventional-changelog --infile ./CHANGELOG.md --same-file --release-count 0 --output-unreleased" }, "dependencies": { + "@babel/template": "^7.4.4", "babel-literal-to-ast": "^2.1.0", "babel-plugin-macros": "^2.5.0", "graphql-tag": "^2.10.1" diff --git a/src/__tests__/__snapshots__/macro.test.js.snap b/src/__tests__/__snapshots__/macro.test.js.snap index 4adb580..c91b5d5 100644 --- a/src/__tests__/__snapshots__/macro.test.js.snap +++ b/src/__tests__/__snapshots__/macro.test.js.snap @@ -1,5 +1,153 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`macros [gql] should handle duplicated fragments (issue#90): [gql] should handle duplicated fragments (issue#90) 1`] = ` + +import { gql } from 'graphql.macro'; +const frag1 = gql\`fragment TestDuplicate on Bar { field }\`; +const frag2 = gql\`fragment TestDuplicate on Bar { field }\`; +const query = gql\`{ bar { fieldOne ...TestDuplicate } } \${frag1} \${frag2}\`; + + ↓ ↓ ↓ ↓ ↓ ↓ + +const frag1 = { + "kind": "Document", + "definitions": [{ + "kind": "FragmentDefinition", + "name": { + "kind": "Name", + "value": "TestDuplicate" + }, + "typeCondition": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Bar" + } + }, + "directives": [], + "selectionSet": { + "kind": "SelectionSet", + "selections": [{ + "kind": "Field", + "name": { + "kind": "Name", + "value": "field" + }, + "arguments": [], + "directives": [] + }] + } + }], + "loc": { + "start": 0, + "end": 39, + "source": { + "body": "fragment TestDuplicate on Bar { field }", + "name": "GraphQL request", + "locationOffset": { + "line": 1, + "column": 1 + } + } + } +}; +const frag2 = { + "kind": "Document", + "definitions": [{ + "kind": "FragmentDefinition", + "name": { + "kind": "Name", + "value": "TestDuplicate" + }, + "typeCondition": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Bar" + } + }, + "directives": [], + "selectionSet": { + "kind": "SelectionSet", + "selections": [{ + "kind": "Field", + "name": { + "kind": "Name", + "value": "field" + }, + "arguments": [], + "directives": [] + }] + } + }], + "loc": { + "start": 0, + "end": 39, + "source": { + "body": "fragment TestDuplicate on Bar { field }", + "name": "GraphQL request", + "locationOffset": { + "line": 1, + "column": 1 + } + } + } +}; +const query = { + "kind": "Document", + "definitions": [{ + "kind": "OperationDefinition", + "operation": "query", + "variableDefinitions": [], + "directives": [], + "selectionSet": { + "kind": "SelectionSet", + "selections": [{ + "kind": "Field", + "name": { + "kind": "Name", + "value": "bar" + }, + "arguments": [], + "directives": [], + "selectionSet": { + "kind": "SelectionSet", + "selections": [{ + "kind": "Field", + "name": { + "kind": "Name", + "value": "fieldOne" + }, + "arguments": [], + "directives": [] + }, { + "kind": "FragmentSpread", + "name": { + "kind": "Name", + "value": "TestDuplicate" + }, + "directives": [] + }] + } + }] + } + }].concat(frag1.definitions, frag2.definitions).reduce((acc, definition) => definition.kind !== "FragmentDefinition" || acc.find(curDef => curDef.name.value === definition.name.value) ? acc : acc.concat(definition), []), + "loc": { + "start": 0, + "end": 39, + "source": { + "body": "{ bar { fieldOne ...TestDuplicate } } ", + "name": "GraphQL request", + "locationOffset": { + "line": 1, + "column": 1 + } + } + } +}; + +`; + exports[`macros [gql] with fragment: [gql] with fragment 1`] = ` import { gql } from 'graphql.macro'; @@ -114,7 +262,7 @@ const query = { } }] } - }].concat(userFragment.definitions), + }].concat(userFragment.definitions).reduce((acc, definition) => definition.kind !== "FragmentDefinition" || acc.find(curDef => curDef.name.value === definition.name.value) ? acc : acc.concat(definition), []), "loc": { "start": 0, "end": 82, diff --git a/src/__tests__/macro.test.js b/src/__tests__/macro.test.js index a1d30e6..edc0a23 100644 --- a/src/__tests__/macro.test.js +++ b/src/__tests__/macro.test.js @@ -53,6 +53,15 @@ pluginTester({ \`; `, }, + '[gql] should handle duplicated fragments (issue#90)': { + error: false, + code: ` + import { gql } from '../macro'; + const frag1 = gql\`fragment TestDuplicate on Bar { field }\`; + const frag2 = gql\`fragment TestDuplicate on Bar { field }\`; + const query = gql\`{ bar { fieldOne ...TestDuplicate } } $\{frag1} $\{frag2}\`; + `, + }, '[loader] without fragment': { error: false, code: ` diff --git a/src/utils/compileWithFragment.js b/src/utils/compileWithFragment.js index 97db80b..0fb4b74 100644 --- a/src/utils/compileWithFragment.js +++ b/src/utils/compileWithFragment.js @@ -1,6 +1,15 @@ // @flow import gqlTag from 'graphql-tag'; import serialize from 'babel-literal-to-ast'; +import template from '@babel/template'; + +const uniqueFn = template.ast(` + (acc, definition) => + definition.kind !== "FragmentDefinition" || + acc.find(curDef => curDef.name.value === definition.name.value) + ? acc + : acc.concat(definition) +`); /** * ref: https://github.com/leoasis/graphql-tag.macro @@ -23,8 +32,14 @@ export default function compileWithFragment( t.memberExpression(expression.node, t.identifier('definitions')), ); definitionsProperty.value = t.callExpression( - t.memberExpression(definitionsArray, t.identifier('concat')), - concatDefinitions, + t.memberExpression( + t.callExpression( + t.memberExpression(definitionsArray, t.identifier('concat')), + concatDefinitions, + ), + t.identifier('reduce'), + ), + [t.toExpression(uniqueFn), t.arrayExpression([])], ); } diff --git a/yarn.lock b/yarn.lock index 8dcfb58..d55bf8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -290,6 +290,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.3.tgz#092d450db02bdb6ccb1ca8ffd47d8774a91aef87" integrity sha512-xsH1CJoln2r74hR+y7cg2B5JCPaTh+Hd+EbBRk9nWGSNspuo6krjhX0Om6RnRQuIvFq8wVXCLKH3kwKDYhanSg== +"@babel/parser@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.4.tgz#5977129431b8fe33471730d255ce8654ae1250b6" + integrity sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w== + "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" @@ -745,6 +750,15 @@ "@babel/parser" "^7.2.2" "@babel/types" "^7.2.2" +"@babel/template@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" + integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.1.6", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3": version "7.2.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" @@ -778,6 +792,15 @@ lodash "^4.17.11" to-fast-properties "^2.0.0" +"@babel/types@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" + integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ== + dependencies: + esutils "^2.0.2" + lodash "^4.17.11" + to-fast-properties "^2.0.0" + "@iamstarkov/listr-update-renderer@0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz#d7c48092a2dcf90fd672b6c8b458649cb350c77e" From acca2a70d8bebb881bbed4c84760a4789afa6f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Hsu=20=28=E5=BE=90=E6=89=BF=E5=BF=97=29?= Date: Mon, 20 May 2019 17:19:04 +0800 Subject: [PATCH 2/2] fix coverage error --- package.json | 2 +- src/utils/compileWithFragment.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 94c4d87..ed4d054 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test": "NODE_ENV='test' jest --coverage", "test:watch": "npm run test -- --watch", "flow": "flow", - "flow-coverage": "hsu-scripts flow --threshold 71", + "flow-coverage": "hsu-scripts flow --threshold 70", "eslint": "eslint ./", "format": "prettier --write '**/*.{js,json,md,css,yaml,yml}' '*.{js,json,md,css,yaml,yml}'", "changelog": "conventional-changelog --infile ./CHANGELOG.md --same-file --release-count 0 --output-unreleased" diff --git a/src/utils/compileWithFragment.js b/src/utils/compileWithFragment.js index 0fb4b74..bdd619a 100644 --- a/src/utils/compileWithFragment.js +++ b/src/utils/compileWithFragment.js @@ -3,6 +3,9 @@ import gqlTag from 'graphql-tag'; import serialize from 'babel-literal-to-ast'; import template from '@babel/template'; +/** + * ref: https://github.com/gajus/babel-plugin-graphql-tag/blob/35edbae44990bf20be2de7139dc0ce5843f70bff/src/index.js#L25 + */ const uniqueFn = template.ast(` (acc, definition) => definition.kind !== "FragmentDefinition" ||