From bdc531d501ded1088630c74acef6e87a5f7cdb90 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Thu, 5 Feb 2026 12:35:56 +0000 Subject: [PATCH 1/5] add document cache for graphql tag --- .../src/__tests__/index.test.ts | 21 +++++++++++++++++++ packages/graphql-js-tag/src/index.ts | 18 +++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/graphql-js-tag/src/__tests__/index.test.ts b/packages/graphql-js-tag/src/__tests__/index.test.ts index f411540d1..7cd916d5f 100644 --- a/packages/graphql-js-tag/src/__tests__/index.test.ts +++ b/packages/graphql-js-tag/src/__tests__/index.test.ts @@ -63,4 +63,25 @@ describe(graphql, () => { const expected = SomeFragment.definitions[0]; expect(actual).toBe(expected); }); + + it("should return the same document for the same template literal", () => { + // This is important for Apollo Client 3.8+ which uses reference equality. + // When a component renders multiple times, the same template literal + // returns the same TemplateStringsArray, so we get the same cached document. + + const doc1 = graphql` + query TestQueryWithFragment { + ...SomeFragment + } + ${SomeFragment} + ` + const doc2 = graphql` + query TestQueryWithFragment { + ...SomeFragment + } + ${SomeFragment} + `; + expect(doc1).toBe(doc2); + }); + }); diff --git a/packages/graphql-js-tag/src/index.ts b/packages/graphql-js-tag/src/index.ts index 0ae4b857a..fff530669 100644 --- a/packages/graphql-js-tag/src/index.ts +++ b/packages/graphql-js-tag/src/index.ts @@ -2,6 +2,10 @@ import { parse } from "graphql"; import type { DocumentNode } from "graphql"; import invariant from "invariant"; +// The same cache as Apollo Client's `graphql-tag` package. https://github.com/apollographql/graphql-tag/blob/main/src/index.ts#L10 +// A map docString -> graphql document +const docCache = new Map(); + /** * This tagged template function is used to capture a single GraphQL document, such as an operation or a fragment. When * a document refers to fragments, those should be interpolated as trailing components, but *no* other interpolation is @@ -49,10 +53,22 @@ export function graphql( document.map((s) => s.trim()).filter((s) => s.length > 0).length === 1, "Interpolations are only allowed at the end of the template.", ); + + const cached = docCache.get(document[0]); + if (cached) { + return cached; + } + const documentNode = parse(document[0], { noLocation: true }); const definitions = new Set(documentNode.definitions); fragments.forEach((doc) => doc.definitions.forEach((def) => definitions.add(def)), ); - return { kind: "Document", definitions: Array.from(definitions) }; + const result: DocumentNode = { + kind: "Document", + definitions: Array.from(definitions), + }; + + docCache.set(document[0], result); + return result; } From 30f87a1170c3ce724d09b7f1a0541aeaade23ac7 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Thu, 5 Feb 2026 12:38:48 +0000 Subject: [PATCH 2/5] Change files --- ...raphql-js-tag-a1a20c6d-7a57-42da-9534-139b3627cad7.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@graphitation-graphql-js-tag-a1a20c6d-7a57-42da-9534-139b3627cad7.json diff --git a/change/@graphitation-graphql-js-tag-a1a20c6d-7a57-42da-9534-139b3627cad7.json b/change/@graphitation-graphql-js-tag-a1a20c6d-7a57-42da-9534-139b3627cad7.json new file mode 100644 index 000000000..2375c81fa --- /dev/null +++ b/change/@graphitation-graphql-js-tag-a1a20c6d-7a57-42da-9534-139b3627cad7.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "add document cache for graphql tag", + "packageName": "@graphitation/graphql-js-tag", + "email": "pavelglac@gmail.com", + "dependentChangeType": "patch" +} From b278bac5db441689d6050da20ff890db85fd7f09 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Thu, 5 Feb 2026 12:56:12 +0000 Subject: [PATCH 3/5] fix formatting --- .../src/__tests__/index.test.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/graphql-js-tag/src/__tests__/index.test.ts b/packages/graphql-js-tag/src/__tests__/index.test.ts index 7cd916d5f..1a12e1147 100644 --- a/packages/graphql-js-tag/src/__tests__/index.test.ts +++ b/packages/graphql-js-tag/src/__tests__/index.test.ts @@ -70,18 +70,17 @@ describe(graphql, () => { // returns the same TemplateStringsArray, so we get the same cached document. const doc1 = graphql` - query TestQueryWithFragment { - ...SomeFragment - } - ${SomeFragment} - ` + query TestQueryWithFragment { + ...SomeFragment + } + ${SomeFragment} + `; const doc2 = graphql` - query TestQueryWithFragment { - ...SomeFragment - } - ${SomeFragment} - `; + query TestQueryWithFragment { + ...SomeFragment + } + ${SomeFragment} + `; expect(doc1).toBe(doc2); }); - }); From d330177a8ee77f73616068895d5ca56bdf30cc88 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Thu, 5 Feb 2026 15:38:47 +0000 Subject: [PATCH 4/5] Handle cases when fragments with the same name have different content --- .../src/__tests__/index.test.ts | 38 +++++++++++++++++++ packages/graphql-js-tag/src/index.ts | 31 +++++++++++---- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/packages/graphql-js-tag/src/__tests__/index.test.ts b/packages/graphql-js-tag/src/__tests__/index.test.ts index 1a12e1147..99fdfc0cf 100644 --- a/packages/graphql-js-tag/src/__tests__/index.test.ts +++ b/packages/graphql-js-tag/src/__tests__/index.test.ts @@ -83,4 +83,42 @@ describe(graphql, () => { `; expect(doc1).toBe(doc2); }); + + it("should return different documents for fragment definitions with the same name but different content", () => { + const HackyFragment = graphql` + fragment SomeFragment on SomeType { + id + name + } + `; + const doc1 = graphql` + query TestQueryWithFragment { + ...SomeFragment + } + ${SomeFragment} + `; + const doc2 = graphql` + query TestQueryWithFragment { + ...SomeFragment + } + ${HackyFragment} + `; + expect(doc1).not.toBe(doc2); + }); + + it("should return the same document for the same template literal without any fragments", () => { + const SomeFragment1 = graphql` + fragment SomeFragment on SomeType { + id + name + } + `; + const SomeFragment2 = graphql` + fragment SomeFragment on SomeType { + id + name + } + `; + expect(SomeFragment1).toBe(SomeFragment2); + }); }); diff --git a/packages/graphql-js-tag/src/index.ts b/packages/graphql-js-tag/src/index.ts index fff530669..b1e91b6b3 100644 --- a/packages/graphql-js-tag/src/index.ts +++ b/packages/graphql-js-tag/src/index.ts @@ -2,9 +2,10 @@ import { parse } from "graphql"; import type { DocumentNode } from "graphql"; import invariant from "invariant"; -// The same cache as Apollo Client's `graphql-tag` package. https://github.com/apollographql/graphql-tag/blob/main/src/index.ts#L10 -// A map docString -> graphql document -const docCache = new Map(); +// The similiar cache as Apollo Client's `graphql-tag` package. https://github.com/apollographql/graphql-tag/blob/main/src/index.ts#L10 +// A map docString -> array of cached entries with their fragments +type CacheEntry = { fragments: DocumentNode[]; result: DocumentNode }; +const docCache = new Map(); /** * This tagged template function is used to capture a single GraphQL document, such as an operation or a fragment. When @@ -54,11 +55,21 @@ export function graphql( "Interpolations are only allowed at the end of the template.", ); - const cached = docCache.get(document[0]); - if (cached) { - return cached; + const cacheEntries = docCache.get(document[0]); + + // We are also handling the case where the same document is used with different interpolated fragments. + if (cacheEntries) { + for (const entry of cacheEntries) { + const areReferencesEqual = entry.fragments.every( + (frag, i) => frag === fragments[i], + ); + if (areReferencesEqual) { + return entry.result; + } + } } + // Parse and create new document const documentNode = parse(document[0], { noLocation: true }); const definitions = new Set(documentNode.definitions); fragments.forEach((doc) => @@ -69,6 +80,12 @@ export function graphql( definitions: Array.from(definitions), }; - docCache.set(document[0], result); + if (cacheEntries) { + // Operation was already cached, but with different fragment interpolations. Add a new cache entry for this combination of fragments. + cacheEntries.push({ fragments, result }); + } else { + docCache.set(document[0], [{ fragments, result }]); + } + return result; } From 020e906fab4f92e23e750576cdf89cb5a3f3adf6 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Thu, 5 Feb 2026 15:50:39 +0000 Subject: [PATCH 5/5] check also the length --- packages/graphql-js-tag/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/graphql-js-tag/src/index.ts b/packages/graphql-js-tag/src/index.ts index b1e91b6b3..9ac1a6aaf 100644 --- a/packages/graphql-js-tag/src/index.ts +++ b/packages/graphql-js-tag/src/index.ts @@ -60,9 +60,9 @@ export function graphql( // We are also handling the case where the same document is used with different interpolated fragments. if (cacheEntries) { for (const entry of cacheEntries) { - const areReferencesEqual = entry.fragments.every( - (frag, i) => frag === fragments[i], - ); + const areReferencesEqual = + entry.fragments.length === fragments.length && + entry.fragments.every((frag, i) => frag === fragments[i]); if (areReferencesEqual) { return entry.result; }