From 9c3d761e93fff6c2c588ba41ab158152bf920935 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 May 2024 17:37:05 -0600 Subject: [PATCH] Better handle errors from the client and detect multiple operations (#435) --- .changeset/khaki-birds-tap.md | 5 ++ .changeset/perfect-lies-protect.md | 5 ++ package-lock.json | 32 ++++++-- .../package.json | 1 + .../src/__tests__/cli.test.ts | 78 +++++++++++++++++++ .../src/index.ts | 45 ++++++++++- 6 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 .changeset/khaki-birds-tap.md create mode 100644 .changeset/perfect-lies-protect.md diff --git a/.changeset/khaki-birds-tap.md b/.changeset/khaki-birds-tap.md new file mode 100644 index 00000000..1851f076 --- /dev/null +++ b/.changeset/khaki-birds-tap.md @@ -0,0 +1,5 @@ +--- +"@apollo/generate-persisted-query-manifest": patch +--- + +Better report errors that originate from Apollo Client during manifest generation. diff --git a/.changeset/perfect-lies-protect.md b/.changeset/perfect-lies-protect.md new file mode 100644 index 00000000..bd67ebd7 --- /dev/null +++ b/.changeset/perfect-lies-protect.md @@ -0,0 +1,5 @@ +--- +"@apollo/generate-persisted-query-manifest": patch +--- + +Detect multiple operations during manifest generation and report them as errors. diff --git a/package-lock.json b/package-lock.json index aeca08a4..edce996b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10544,7 +10544,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -10555,8 +10554,7 @@ "node_modules/lru-cache/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/make-dir": { "version": "4.0.0", @@ -14116,6 +14114,7 @@ "cosmiconfig-typescript-loader": "^5.0.0", "globby": "^11.1.0", "lodash": "^4.17.21", + "semver": "^7.6.0", "vfile": "^4.2.1", "vfile-reporter": "^6.0.2" }, @@ -14189,6 +14188,20 @@ "node": ">=8" } }, + "packages/generate-persisted-query-manifest/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "packages/generate-persisted-query-manifest/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -14428,6 +14441,7 @@ "cosmiconfig-typescript-loader": "^5.0.0", "globby": "^11.1.0", "lodash": "^4.17.21", + "semver": "^7.6.0", "vfile": "^4.2.1", "vfile-reporter": "^6.0.2" }, @@ -14467,6 +14481,14 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "requires": { + "lru-cache": "^6.0.0" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -22127,7 +22149,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" }, @@ -22135,8 +22156,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, diff --git a/packages/generate-persisted-query-manifest/package.json b/packages/generate-persisted-query-manifest/package.json index 9c078c0a..580d7452 100644 --- a/packages/generate-persisted-query-manifest/package.json +++ b/packages/generate-persisted-query-manifest/package.json @@ -32,6 +32,7 @@ "cosmiconfig-typescript-loader": "^5.0.0", "globby": "^11.1.0", "lodash": "^4.17.21", + "semver": "^7.6.0", "vfile": "^4.2.1", "vfile-reporter": "^6.0.2" }, diff --git a/packages/generate-persisted-query-manifest/src/__tests__/cli.test.ts b/packages/generate-persisted-query-manifest/src/__tests__/cli.test.ts index 46c90ea2..cc22aeeb 100644 --- a/packages/generate-persisted-query-manifest/src/__tests__/cli.test.ts +++ b/packages/generate-persisted-query-manifest/src/__tests__/cli.test.ts @@ -1419,6 +1419,84 @@ const query; await cleanup(); }); +test("does not allow multiple operations in a single document", async () => { + const { cleanup, runCommand, writeFile } = await setup(); + + await writeFile( + "./src/queries.graphql", + ` +query GreetingQuery { + greeting +} + +query GoodbyeQuery { + goodbye +} +`, + ); + + await writeFile( + "./src/mutations.graphql", + ` +mutation SayHello { + sayHello +} + +mutation SayGoodbye { + goodbye +} +`, + ); + + await writeFile( + "./src/subscriptions.graphql", + ` +subscription HelloSubscription { + hello +} + +subscription GoodbyeSubscription { + goodbye +} +`, + ); + await writeFile( + "./src/mixed.graphql", + ` +mutation TestMutation { + test +} + +query TestQuery { + test +} + +subscription TestSubscription { + test +} +`, + ); + + const { code, stderr } = await runCommand(); + + expect(code).toBe(1); + expect(stderr).toMatchInlineSnapshot(` + [ + "src/mixed.graphql", + "1:1 error Cannot declare multiple operations in a single document.", + "src/mutations.graphql", + "1:1 error Cannot declare multiple operations in a single document.", + "src/queries.graphql", + "1:1 error Cannot declare multiple operations in a single document.", + "src/subscriptions.graphql", + "1:1 error Cannot declare multiple operations in a single document.", + "✖ 4 errors", + ] + `); + + await cleanup(); +}); + test("gathers and reports all errors together", async () => { const { cleanup, runCommand, writeFile } = await setup(); const anonymousQuery = gql` diff --git a/packages/generate-persisted-query-manifest/src/index.ts b/packages/generate-persisted-query-manifest/src/index.ts index 2ee9b47f..101d3ecd 100644 --- a/packages/generate-persisted-query-manifest/src/index.ts +++ b/packages/generate-persisted-query-manifest/src/index.ts @@ -1,5 +1,6 @@ import { createFragmentRegistry } from "@apollo/client/cache"; import type { FragmentRegistryAPI } from "@apollo/client/cache"; +import semver from "semver"; import { ApolloClient, ApolloLink, @@ -21,6 +22,7 @@ import { type DocumentNode, visit, GraphQLError, + BREAK, } from "graphql"; import { first, sortBy } from "lodash"; import { createHash } from "node:crypto"; @@ -259,10 +261,22 @@ const ERROR_MESSAGES = { )}".`; }, parseError(error: Error) { - return `${error.name}: ${error.message}`; + return formatErrorMessage(error); + }, + multipleOperations() { + return "Cannot declare multiple operations in a single document."; }, }; +async function enableDevMessages() { + const { loadDevMessages, loadErrorMessages } = await import( + "@apollo/client/dev" + ); + + loadDevMessages(); + loadErrorMessages(); +} + function addError( source: Pick & Partial, message: string, @@ -372,6 +386,10 @@ function uniq(arr: T[]) { return [...new Set(arr)]; } +function formatErrorMessage(error: Error) { + return `${error.name}: ${error.message}`; +} + async function fromFilepathList( documents: string | string[], ): Promise { @@ -385,6 +403,8 @@ async function fromFilepathList( continue; } + let documentCount = 0; + visit(source.node, { FragmentDefinition(node) { const name = node.name.value; @@ -404,6 +424,11 @@ async function fromFilepathList( OperationDefinition(node) { const name = node.name?.value; + if (++documentCount > 1) { + addError(source, ERROR_MESSAGES.multipleOperations()); + return BREAK; + } + if (!name) { addError(source, ERROR_MESSAGES.anonymousOperation(node)); @@ -560,15 +585,29 @@ export async function generatePersistedQueryManifest( }), }); + if (semver.gte(client.version, "3.8.0")) { + await enableDevMessages(); + } + for (const [_, sources] of sortBy([...operationsByName.entries()], first)) { for (const source of sources) { if (source.node) { - await client.query({ query: source.node, fetchPolicy: "no-cache" }); + try { + await client.query({ query: source.node, fetchPolicy: "no-cache" }); + } catch (error) { + if (error instanceof Error) { + addError(source, formatErrorMessage(error)); + } else { + addError(source, "Unknown error occured. Please file a bug."); + } + } } } } - maybeReportErrorsAndExit(configFile); + maybeReportErrorsAndExit( + uniq(sources.map((source) => source.file).concat(configFile)), + ); return { format: "apollo-persisted-query-manifest",