diff --git a/.changeset/hungry-chairs-live.md b/.changeset/hungry-chairs-live.md new file mode 100644 index 00000000000..d3f055b41c5 --- /dev/null +++ b/.changeset/hungry-chairs-live.md @@ -0,0 +1,17 @@ +--- +'@apollo/client': minor +--- + +Add a new mechanism for Error Extraction to reduce bundle size by including +error message texts on an opt-in basis. +By default, errors will link to an error page with the entire error message. +This replaces "development" and "production" errors and works without +additional bundler configuration. +Bundling the text of error messages and development warnings can be enabled by +```js +import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev"; +if (process.env.NODE_ENV !== "production") { + loadErrorMessages(); + loadDevMessages(); +} +``` \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index ef091d26400..ed3a8c3cb6e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -19,12 +19,17 @@ # Do not format anything automatically except files listed below * +!config/ +config/* +!config/processInvariants.ts + ##### PATHS TO BE FORMATTED ##### !src/ src/* !src/react/ src/react/* +!src/dev # Allow src/react/cache/ApolloProvider !src/react/context/ @@ -65,6 +70,9 @@ src/utilities/common/* src/utilities/common/__tests__/* !src/utilities/common/__tests__/omitDeep.ts !src/utilities/common/__tests__/stripTypename.ts +!src/utilities/globals +src/utilities/globals/* +!src/utilities/globals/invariantWrappers.ts !src/utilities/graphql src/utilities/graphql/* !src/utilities/graphql/operations.ts diff --git a/.size-limit.cjs b/.size-limit.cjs index 226c661a36c..b09f8f9d297 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,7 +1,7 @@ const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "37.06kb" + limit: "37460" }, { path: "dist/main.cjs", @@ -10,7 +10,7 @@ const checks = [ { path: "dist/index.js", import: "{ ApolloClient, InMemoryCache, HttpLink }", - limit: "35.31kb" + limit: "33243" }, ...[ "ApolloProvider", diff --git a/config/entryPoints.js b/config/entryPoints.js index 34e3b5fab7b..e33d53ceb00 100644 --- a/config/entryPoints.js +++ b/config/entryPoints.js @@ -2,6 +2,7 @@ const entryPoints = [ { dirs: [], bundleName: "main" }, { dirs: ['cache'] }, { dirs: ['core'] }, + { dirs: ['dev'] }, { dirs: ['errors'] }, { dirs: ['link', 'batch'] }, { dirs: ['link', 'batch-http'] }, diff --git a/config/processInvariants.ts b/config/processInvariants.ts index c63881befc9..3afd82e5438 100644 --- a/config/processInvariants.ts +++ b/config/processInvariants.ts @@ -1,59 +1,123 @@ -import * as fs from "fs"; -import { posix, join as osPathJoin } from "path"; +import * as fs from 'fs'; +import { posix, join as osPathJoin } from 'path'; import { distDir, eachFile, reparse, reprint } from './helpers'; +import type { ExpressionKind } from 'ast-types/lib/gen/kinds'; eachFile(distDir, (file, relPath) => { - const source = fs.readFileSync(file, "utf8"); + const source = fs.readFileSync(file, 'utf8'); const output = transform(source, relPath); if (source !== output) { - fs.writeFileSync(file, output, "utf8"); + fs.writeFileSync(file, output, 'utf8'); } }).then(() => { fs.writeFileSync( - osPathJoin(distDir, "invariantErrorCodes.js"), - recast.print(errorCodeManifest, { + osPathJoin(distDir, 'invariantErrorCodes.js'), + recast.print(program, { tabWidth: 2, - }).code + "\n", + }).code + '\n' ); }); -import * as recast from "recast"; +import * as recast from 'recast'; const b = recast.types.builders; const n = recast.types.namedTypes; type Node = recast.types.namedTypes.Node; -type NumericLiteral = recast.types.namedTypes.NumericLiteral; type CallExpression = recast.types.namedTypes.CallExpression; type NewExpression = recast.types.namedTypes.NewExpression; let nextErrorCode = 1; -const errorCodeManifest = b.objectExpression([ - b.property("init", - b.stringLiteral("@apollo/client version"), - b.stringLiteral(require("../package.json").version), +const program = b.program([]); +const allExports = { + errorCodes: getExportObject('errorCodes'), + devDebug: getExportObject('devDebug'), + devLog: getExportObject('devLog'), + devWarn: getExportObject('devWarn'), + devError: getExportObject('devError'), +}; +type ExportName = keyof typeof allExports; + +allExports.errorCodes.comments = [ + b.commentLine( + ' This file is used by the error message display website and the', + true ), -]); - -errorCodeManifest.comments = [ - b.commentLine(' This file is meant to help with looking up the source of errors like', true), - b.commentLine(' "Invariant Violation: 35" and is automatically generated by the file', true), - b.commentLine(' @apollo/client/config/processInvariants.ts for each @apollo/client', true), - b.commentLine(' release. The numbers may change from release to release, so please', true), - b.commentLine(' consult the @apollo/client/invariantErrorCodes.js file specific to', true), - b.commentLine(' your @apollo/client version. This file is not meant to be imported.', true), + b.commentLine(' @apollo/client/includeErrors entry point.', true), + b.commentLine(' This file is not meant to be imported manually.', true), ]; +function getExportObject(exportName: string) { + const object = b.objectExpression([]); + program.body.push( + b.exportNamedDeclaration( + b.variableDeclaration('const', [ + b.variableDeclarator(b.identifier(exportName), object), + ]) + ) + ); + return object; +} + function getErrorCode( file: string, expr: CallExpression | NewExpression, -): NumericLiteral { - const numLit = b.numericLiteral(nextErrorCode++); - errorCodeManifest.properties.push( - b.property("init", numLit, b.objectExpression([ - b.property("init", b.identifier("file"), b.stringLiteral("@apollo/client/" + file)), - b.property("init", b.identifier("node"), expr), - ])), - ); - return numLit; + type: keyof typeof allExports +): ExpressionKind { + if (isIdWithName(expr.callee, 'invariant')) { + return extractString( + file, + allExports[type].properties, + expr.arguments[1], + expr.arguments[0] + ); + } else { + return extractString(file, allExports[type].properties, expr.arguments[0]); + } + + function extractString( + file: string, + target: typeof allExports[ExportName]['properties'], + message: recast.types.namedTypes.SpreadElement | ExpressionKind, + condition?: recast.types.namedTypes.SpreadElement | ExpressionKind + ): ExpressionKind { + if (message.type === 'ConditionalExpression') { + return b.conditionalExpression( + message.test, + extractString(file, target, message.consequent, condition), + extractString(file, target, message.alternate, condition) + ); + } else if (isStringOnly(message)) { + const obj = b.objectExpression([]); + const numLit = b.numericLiteral(nextErrorCode++); + target.push(b.property('init', numLit, obj)); + + obj.properties.push( + b.property( + 'init', + b.identifier('file'), + b.stringLiteral('@apollo/client/' + file) + ) + ); + if (condition) { + obj.properties.push( + b.property( + 'init', + b.identifier('condition'), + b.stringLiteral(reprint(expr.arguments[0])) + ) + ); + } + obj.properties.push(b.property('init', b.identifier('message'), message)); + + return numLit; + } else { + throw new Error(`invariant minification error: node cannot have dynamical error argument! + file: ${posix.join(distDir, file)}:${expr.loc?.start.line} + code: + + ${reprint(message)} + `); + } + } } function transform(code: string, relativeFilePath: string) { @@ -71,57 +135,67 @@ function transform(code: string, relativeFilePath: string) { this.traverse(path); const node = path.node; - if (isCallWithLength(node, "invariant", 1)) { - if (isDEVConditional(path.parent.node)) { - return; - } + if (isCallWithLength(node, 'invariant', 1)) { + const newArgs = [...node.arguments]; + newArgs.splice( + 1, + 1, + getErrorCode(relativeFilePath, node, 'errorCodes') + ); - const newArgs = node.arguments.slice(0, 1); - newArgs.push(getErrorCode(relativeFilePath, node)); + return b.callExpression.from({ + ...node, + arguments: newArgs, + }); + } - addedDEV = true; - return b.conditionalExpression( - makeDEVExpr(), - node, - b.callExpression.from({ - ...node, - arguments: newArgs, - }), + if (isCallWithLength(node, 'newInvariantError', 0)) { + const newArgs = [...node.arguments]; + newArgs.splice( + 0, + 1, + getErrorCode(relativeFilePath, node, 'errorCodes') ); - } - if (node.callee.type === "MemberExpression" && - isIdWithName(node.callee.object, "invariant") && - isIdWithName(node.callee.property, "debug", "log", "warn", "error")) { - if (isDEVLogicalAnd(path.parent.node)) { - return; - } - addedDEV = true; - return b.logicalExpression("&&", makeDEVExpr(), node); + return b.callExpression.from({ + ...node, + arguments: newArgs, + }); } - }, - visitNewExpression(path) { - this.traverse(path); - const node = path.node; - if (isCallWithLength(node, "InvariantError", 0)) { - if (isDEVConditional(path.parent.node)) { - return; - } + if ( + node.callee.type === 'MemberExpression' && + isIdWithName(node.callee.object, 'invariant') && + isIdWithName(node.callee.property, 'debug', 'log', 'warn', 'error') + ) { + let newNode = node; + if (node.arguments[0].type !== 'Identifier') { + const prop = node.callee.property; + if (!n.Identifier.check(prop)) throw new Error('unexpected type'); - const newArgs = [getErrorCode(relativeFilePath, node)]; - - addedDEV = true; - return b.conditionalExpression( - makeDEVExpr(), - node, - b.newExpression.from({ + const newArgs = [...node.arguments]; + newArgs.splice( + 0, + 1, + getErrorCode( + relativeFilePath, + node, + ('dev' + capitalize(prop.name)) as ExportName + ) + ); + newNode = b.callExpression.from({ ...node, arguments: newArgs, - }), - ); + }); + } + + if (isDEVLogicalAnd(path.parent.node)) { + return newNode; + } + addedDEV = true; + return b.logicalExpression('&&', makeDEVExpr(), newNode); } - } + }, }); if (addedDEV) { @@ -137,24 +211,27 @@ function transform(code: string, relativeFilePath: string) { // Normalize node.source.value relative to the current file. if ( - typeof importedModuleId === "string" && - importedModuleId.startsWith(".") + typeof importedModuleId === 'string' && + importedModuleId.startsWith('.') ) { - const normalized = posix.normalize(posix.join( - posix.dirname(relativeFilePath), - importedModuleId, - )); - if (normalized === "utilities/globals") { + const normalized = posix.normalize( + posix.join(posix.dirname(relativeFilePath), importedModuleId) + ); + if (normalized === 'utilities/globals') { foundExistingImportDecl = true; - if (node.specifiers?.some(s => isIdWithName(s.local || s.id, "__DEV__"))) { + if ( + node.specifiers?.some((s) => + isIdWithName(s.local || s.id, '__DEV__') + ) + ) { return false; } if (!node.specifiers) node.specifiers = []; - node.specifiers.push(b.importSpecifier(b.identifier("__DEV__"))); + node.specifiers.push(b.importSpecifier(b.identifier('__DEV__'))); return false; } } - } + }, }); if (!foundExistingImportDecl) { @@ -162,12 +239,12 @@ function transform(code: string, relativeFilePath: string) { // this code is running at build time, we can simplify things by throwing // here, because we expect invariant and InvariantError to be imported // from the utilities/globals subpackage. - throw new Error(`Missing import from "${ - posix.relative( + throw new Error( + `Missing import from "${posix.relative( posix.dirname(relativeFilePath), - "utilities/globals", - ) - } in ${relativeFilePath}`); + 'utilities/globals' + )} in ${relativeFilePath}` + ); } } @@ -175,34 +252,56 @@ function transform(code: string, relativeFilePath: string) { } function isIdWithName(node: Node | null | undefined, ...names: string[]) { - return node && n.Identifier.check(node) && - names.some(name => name === node.name); + return ( + node && n.Identifier.check(node) && names.some((name) => name === node.name) + ); } function isCallWithLength( node: CallExpression | NewExpression, name: string, - length: number, + length: number ) { - return isIdWithName(node.callee, name) && - node.arguments.length > length; -} - -function isDEVConditional(node: Node) { - return n.ConditionalExpression.check(node) && - isDEVExpr(node.test); + return isIdWithName(node.callee, name) && node.arguments.length > length; } function isDEVLogicalAnd(node: Node) { - return n.LogicalExpression.check(node) && - node.operator === "&&" && - isDEVExpr(node.left); + return ( + n.LogicalExpression.check(node) && + node.operator === '&&' && + isDEVExpr(node.left) + ); } function makeDEVExpr() { - return b.identifier("__DEV__"); + return b.identifier('__DEV__'); } function isDEVExpr(node: Node) { - return isIdWithName(node, "__DEV__"); + return isIdWithName(node, '__DEV__'); +} + +function isStringOnly( + node: recast.types.namedTypes.ASTNode +): node is ExpressionKind { + switch (node.type) { + case 'StringLiteral': + case 'Literal': + return true; + case 'TemplateLiteral': + return (node.expressions as recast.types.namedTypes.ASTNode[]).every( + isStringOnly + ); + case 'BinaryExpression': + return ( + node.operator == '+' && + isStringOnly(node.left) && + isStringOnly(node.right) + ); + } + return false; +} + +function capitalize(str: string) { + return str[0].toUpperCase() + str.slice(1); } diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 4861a92183d..ca997749fb0 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -2,6 +2,7 @@ import gql from 'graphql-tag'; import { ApolloClient, + ApolloError, DefaultOptions, FetchPolicy, QueryOptions, @@ -15,7 +16,7 @@ import { HttpLink } from '../link/http'; import { InMemoryCache } from '../cache'; import { itAsync, withErrorSpy } from '../testing'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; -import { invariant } from 'ts-invariant'; +import { invariant } from '../utilities/globals'; describe('ApolloClient', () => { describe('constructor', () => { @@ -2370,7 +2371,7 @@ describe('ApolloClient', () => { setTimeout(() => { try { expect(invariantDebugSpy).toHaveBeenCalledTimes(1); - expect(invariantDebugSpy).toHaveBeenCalledWith('In client.refetchQueries, Promise.all promise rejected with error ApolloError: refetch failed'); + expect(invariantDebugSpy).toHaveBeenCalledWith('In client.refetchQueries, Promise.all promise rejected with error %o', new ApolloError({errorMessage:"refetch failed"})); resolve(); } catch (err) { reject(err); diff --git a/src/__tests__/__snapshots__/ApolloClient.ts.snap b/src/__tests__/__snapshots__/ApolloClient.ts.snap index 63f2d60b480..a91a2341029 100644 --- a/src/__tests__/__snapshots__/ApolloClient.ts.snap +++ b/src/__tests__/__snapshots__/ApolloClient.ts.snap @@ -213,10 +213,12 @@ exports[`ApolloClient writeFragment should warn when the data provided does not [MockFunction] { "calls": Array [ Array [ - "Missing field 'e' while writing result { - \\"__typename\\": \\"Bar\\", - \\"i\\": 10 -}", + "Missing field '%s' while writing result %o", + "e", + Object { + "__typename": "Bar", + "i": 10, + }, ], ], "results": Array [ @@ -382,11 +384,13 @@ exports[`ApolloClient writeQuery should warn when the data provided does not mat [MockFunction] { "calls": Array [ Array [ - "Missing field 'description' while writing result { - \\"id\\": \\"1\\", - \\"name\\": \\"Todo 1\\", - \\"__typename\\": \\"Todo\\" -}", + "Missing field '%s' while writing result %o", + "description", + Object { + "__typename": "Todo", + "id": "1", + "name": "Todo 1", + }, ], ], "results": Array [ diff --git a/src/__tests__/__snapshots__/client.ts.snap b/src/__tests__/__snapshots__/client.ts.snap index 4e2bfb9219d..2ca16a76a05 100644 --- a/src/__tests__/__snapshots__/client.ts.snap +++ b/src/__tests__/__snapshots__/client.ts.snap @@ -46,12 +46,14 @@ exports[`client should warn if server returns wrong data 1`] = ` [MockFunction] { "calls": Array [ Array [ - "Missing field 'description' while writing result { - \\"id\\": \\"1\\", - \\"name\\": \\"Todo 1\\", - \\"price\\": 100, - \\"__typename\\": \\"Todo\\" -}", + "Missing field '%s' while writing result %o", + "description", + Object { + "__typename": "Todo", + "id": "1", + "name": "Todo 1", + "price": 100, + }, ], ], "results": Array [ diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 096c10ce3be..34fd4df8130 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -137,6 +137,14 @@ Array [ ] `; +exports[`exports of public entry points @apollo/client/dev 1`] = ` +Array [ + "loadDevMessages", + "loadErrorMessageHandler", + "loadErrorMessages", +] +`; + exports[`exports of public entry points @apollo/client/errors 1`] = ` Array [ "ApolloError", @@ -470,5 +478,6 @@ Array [ "global", "invariant", "maybe", + "newInvariantError", ] `; diff --git a/src/__tests__/__snapshots__/mutationResults.ts.snap b/src/__tests__/__snapshots__/mutationResults.ts.snap index 887a5147f14..cf6e8ea4853 100644 --- a/src/__tests__/__snapshots__/mutationResults.ts.snap +++ b/src/__tests__/__snapshots__/mutationResults.ts.snap @@ -4,11 +4,13 @@ exports[`mutation results should warn when the result fields don't match the que [MockFunction] { "calls": Array [ Array [ - "Missing field 'description' while writing result { - \\"id\\": \\"2\\", - \\"name\\": \\"Todo 2\\", - \\"__typename\\": \\"createTodo\\" -}", + "Missing field '%s' while writing result %o", + "description", + Object { + "__typename": "createTodo", + "id": "2", + "name": "Todo 2", + }, ], ], "results": Array [ diff --git a/src/__tests__/exports.ts b/src/__tests__/exports.ts index eb64c333b1e..3b5ce9723b1 100644 --- a/src/__tests__/exports.ts +++ b/src/__tests__/exports.ts @@ -6,6 +6,7 @@ import * as cache from "../cache"; import * as client from ".."; import * as core from "../core"; +import * as dev from '../dev'; import * as errors from "../errors"; import * as linkBatch from "../link/batch"; import * as linkBatchHTTP from "../link/batch-http"; @@ -49,6 +50,7 @@ describe('exports of public entry points', () => { check("@apollo/client", client); check("@apollo/client/cache", cache); check("@apollo/client/core", core); + check("@apollo/client/dev", dev); check("@apollo/client/errors", errors); check("@apollo/client/link/batch", linkBatch); check("@apollo/client/link/batch-http", linkBatchHTTP); diff --git a/src/__tests__/local-state/__snapshots__/export.ts.snap b/src/__tests__/local-state/__snapshots__/export.ts.snap index 08bd4c63121..b73dd28148c 100644 --- a/src/__tests__/local-state/__snapshots__/export.ts.snap +++ b/src/__tests__/local-state/__snapshots__/export.ts.snap @@ -4,14 +4,18 @@ exports[`@client @export tests should NOT refetch if an @export variable has not [MockFunction] { "calls": Array [ Array [ - "Missing field 'postCount' while writing result { - \\"currentAuthorId\\": 101 -}", + "Missing field '%s' while writing result %o", + "postCount", + Object { + "currentAuthorId": 101, + }, ], Array [ - "Missing field 'postCount' while writing result { - \\"currentAuthorId\\": 100 -}", + "Missing field '%s' while writing result %o", + "postCount", + Object { + "currentAuthorId": 100, + }, ], ], "results": Array [ @@ -31,13 +35,15 @@ exports[`@client @export tests should allow @client @export variables to be used [MockFunction] { "calls": Array [ Array [ - "Missing field 'postCount' while writing result { - \\"currentAuthor\\": { - \\"name\\": \\"John Smith\\", - \\"authorId\\": 100, - \\"__typename\\": \\"Author\\" - } -}", + "Missing field '%s' while writing result %o", + "postCount", + Object { + "currentAuthor": Object { + "__typename": "Author", + "authorId": 100, + "name": "John Smith", + }, + }, ], ], "results": Array [ @@ -53,63 +59,79 @@ exports[`@client @export tests should refetch if an @export variable changes, th [MockFunction] { "calls": Array [ Array [ - "Missing field 'postCount' while writing result { - \\"appContainer\\": { - \\"systemDetails\\": { - \\"currentAuthor\\": { - \\"name\\": \\"John Smith\\", - \\"authorId\\": 100, - \\"__typename\\": \\"Author\\" + "Missing field '%s' while writing result %o", + "postCount", + Object { + "appContainer": Object { + "__typename": "AppContainer", + "systemDetails": Object { + "__typename": "SystemDetails", + "currentAuthor": Object { + "__typename": "Author", + "authorId": 100, + "name": "John Smith", + }, + }, + }, }, - \\"__typename\\": \\"SystemDetails\\" - }, - \\"__typename\\": \\"AppContainer\\" - } -}", ], Array [ - "Missing field 'title' while writing result { - \\"loggedInReviewerId\\": 100, - \\"__typename\\": \\"Post\\", - \\"id\\": 10 -}", + "Missing field '%s' while writing result %o", + "title", + Object { + "__typename": "Post", + "id": 10, + "loggedInReviewerId": 100, + }, ], Array [ - "Missing field 'reviewerDetails' while writing result { - \\"postRequiringReview\\": { - \\"loggedInReviewerId\\": 100, - \\"__typename\\": \\"Post\\", - \\"id\\": 10 - } -}", + "Missing field '%s' while writing result %o", + "reviewerDetails", + Object { + "postRequiringReview": Object { + "__typename": "Post", + "id": 10, + "loggedInReviewerId": 100, + }, + }, ], Array [ - "Missing field 'id' while writing result { - \\"__typename\\": \\"Post\\" -}", + "Missing field '%s' while writing result %o", + "id", + Object { + "__typename": "Post", + }, ], Array [ - "Missing field 'title' while writing result { - \\"__typename\\": \\"Post\\" -}", + "Missing field '%s' while writing result %o", + "title", + Object { + "__typename": "Post", + }, ], Array [ - "Missing field 'reviewerDetails' while writing result { - \\"postRequiringReview\\": { - \\"__typename\\": \\"Post\\" - } -}", + "Missing field '%s' while writing result %o", + "reviewerDetails", + Object { + "postRequiringReview": Object { + "__typename": "Post", + }, + }, ], Array [ - "Missing field 'post' while writing result { - \\"primaryReviewerId\\": 100, - \\"secondaryReviewerId\\": 200 -}", + "Missing field '%s' while writing result %o", + "post", + Object { + "primaryReviewerId": 100, + "secondaryReviewerId": 200, + }, ], Array [ - "Missing field 'postCount' while writing result { - \\"currentAuthorId\\": 100 -}", + "Missing field '%s' while writing result %o", + "postCount", + Object { + "currentAuthorId": 100, + }, ], ], "results": Array [ diff --git a/src/__tests__/local-state/__snapshots__/general.ts.snap b/src/__tests__/local-state/__snapshots__/general.ts.snap index 331aef47fe9..6c69495e267 100644 --- a/src/__tests__/local-state/__snapshots__/general.ts.snap +++ b/src/__tests__/local-state/__snapshots__/general.ts.snap @@ -8,9 +8,11 @@ exports[`Combining client and server state/operations should handle a simple que [MockFunction] { "calls": Array [ Array [ - "Missing field 'lastCount' while writing result { - \\"count\\": 0 -}", + "Missing field '%s' while writing result %o", + "lastCount", + Object { + "count": 0, + }, ], ], "results": Array [ @@ -26,11 +28,13 @@ exports[`Combining client and server state/operations should support nested quer [MockFunction] { "calls": Array [ Array [ - "Missing field 'lastName' while writing result { - \\"__typename\\": \\"User\\", - \\"id\\": 123, - \\"firstName\\": \\"John\\" -}", + "Missing field '%s' while writing result %o", + "lastName", + Object { + "__typename": "User", + "firstName": "John", + "id": 123, + }, ], ], "results": Array [ diff --git a/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap index 3ed68fa48cf..c222c6f47d5 100644 --- a/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap +++ b/src/cache/inmemory/__tests__/__snapshots__/policies.ts.snap @@ -54,29 +54,33 @@ exports[`type policies complains about missing key fields 1`] = ` [MockFunction] { "calls": Array [ Array [ - "Missing field 'title' while writing result { - \\"year\\": 2011, - \\"theInformationBookData\\": { - \\"__typename\\": \\"Book\\", - \\"isbn\\": \\"1400096235\\", - \\"title\\": \\"The Information\\", - \\"subtitle\\": \\"A History, a Theory, a Flood\\", - \\"author\\": { - \\"name\\": \\"James Gleick\\" - } - } -}", + "Missing field '%s' while writing result %o", + "title", + Object { + "theInformationBookData": Object { + "__typename": "Book", + "author": Object { + "name": "James Gleick", + }, + "isbn": "1400096235", + "subtitle": "A History, a Theory, a Flood", + "title": "The Information", + }, + "year": 2011, + }, ], Array [ - "Missing field 'year' while writing result { - \\"__typename\\": \\"Book\\", - \\"isbn\\": \\"1400096235\\", - \\"title\\": \\"The Information\\", - \\"subtitle\\": \\"A History, a Theory, a Flood\\", - \\"author\\": { - \\"name\\": \\"James Gleick\\" - } -}", + "Missing field '%s' while writing result %o", + "year", + Object { + "__typename": "Book", + "author": Object { + "name": "James Gleick", + }, + "isbn": "1400096235", + "subtitle": "A History, a Theory, a Flood", + "title": "The Information", + }, ], ], "results": Array [ @@ -1280,11 +1284,13 @@ exports[`type policies field policies readField helper function calls custom rea [MockFunction] { "calls": Array [ Array [ - "Missing field 'blockers' while writing result { - \\"__typename\\": \\"Task\\", - \\"id\\": 4, - \\"description\\": \\"grandchild task\\" -}", + "Missing field '%s' while writing result %o", + "blockers", + Object { + "__typename": "Task", + "description": "grandchild task", + "id": 4, + }, ], ], "results": Array [ @@ -1300,34 +1306,40 @@ exports[`type policies field policies runs nested merge functions as well as anc [MockFunction] { "calls": Array [ Array [ - "Missing field 'time' while writing result { - \\"__typename\\": \\"Event\\", - \\"id\\": 123 -}", + "Missing field '%s' while writing result %o", + "time", + Object { + "__typename": "Event", + "id": 123, + }, ], Array [ - "Missing field 'time' while writing result { - \\"__typename\\": \\"Event\\", - \\"id\\": 345, - \\"name\\": \\"Rooftop dog party\\", - \\"attendees\\": [ - { - \\"__typename\\": \\"Attendee\\", - \\"id\\": 456, - \\"name\\": \\"Inspector Beckett\\" - }, - { - \\"__typename\\": \\"Attendee\\", - \\"id\\": 234 - } - ] -}", + "Missing field '%s' while writing result %o", + "time", + Object { + "__typename": "Event", + "attendees": Array [ + Object { + "__typename": "Attendee", + "id": 456, + "name": "Inspector Beckett", + }, + Object { + "__typename": "Attendee", + "id": 234, + }, + ], + "id": 345, + "name": "Rooftop dog party", + }, ], Array [ - "Missing field 'name' while writing result { - \\"__typename\\": \\"Attendee\\", - \\"id\\": 234 -}", + "Missing field '%s' while writing result %o", + "name", + Object { + "__typename": "Attendee", + "id": 234, + }, ], ], "results": Array [ @@ -1351,10 +1363,12 @@ exports[`type policies readField warns if explicitly passed undefined \`from\` o [MockFunction] { "calls": Array [ Array [ - "Undefined 'from' passed to readField with arguments [{\\"fieldName\\":\\"firstName\\",\\"from\\":}]", + "Undefined 'from' passed to readField with arguments %s", + "[{\\"fieldName\\":\\"firstName\\",\\"from\\":}]", ], Array [ - "Undefined 'from' passed to readField with arguments [\\"lastName\\",]", + "Undefined 'from' passed to readField with arguments %s", + "[\\"lastName\\",]", ], ], "results": Array [ diff --git a/src/cache/inmemory/__tests__/__snapshots__/roundtrip.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/roundtrip.ts.snap index 9f5f5e848ca..5b3fa1f5a5c 100644 --- a/src/cache/inmemory/__tests__/__snapshots__/roundtrip.ts.snap +++ b/src/cache/inmemory/__tests__/__snapshots__/roundtrip.ts.snap @@ -4,11 +4,13 @@ exports[`roundtrip fragments should throw an error on two of the same inline fra [MockFunction] { "calls": Array [ Array [ - "Missing field 'rank' while writing result { - \\"__typename\\": \\"Jedi\\", - \\"name\\": \\"Luke Skywalker\\", - \\"side\\": \\"bright\\" -}", + "Missing field '%s' while writing result %o", + "rank", + Object { + "__typename": "Jedi", + "name": "Luke Skywalker", + "side": "bright", + }, ], ], "results": Array [ @@ -24,11 +26,13 @@ exports[`roundtrip fragments should throw on error on two of the same spread fra [MockFunction] { "calls": Array [ Array [ - "Missing field 'rank' while writing result { - \\"__typename\\": \\"Jedi\\", - \\"name\\": \\"Luke Skywalker\\", - \\"side\\": \\"bright\\" -}", + "Missing field '%s' while writing result %o", + "rank", + Object { + "__typename": "Jedi", + "name": "Luke Skywalker", + "side": "bright", + }, ], ], "results": Array [ diff --git a/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap b/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap index f4d2f946cb6..c5dcfb3571a 100644 --- a/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap +++ b/src/cache/inmemory/__tests__/__snapshots__/writeToStore.ts.snap @@ -75,10 +75,12 @@ exports[`writing to the store should not keep reference when type of mixed inlin [MockFunction] { "calls": Array [ Array [ - "Missing field 'id' while writing result { - \\"__typename\\": \\"Cat\\", - \\"name\\": \\"cat\\" -}", + "Missing field '%s' while writing result %o", + "id", + Object { + "__typename": "Cat", + "name": "cat", + }, ], ], "results": Array [ @@ -205,12 +207,14 @@ exports[`writing to the store writeResultToStore shape checking should warn when [MockFunction] { "calls": Array [ Array [ - "Missing field 'price' while writing result { - \\"id\\": \\"1\\", - \\"name\\": \\"Todo 1\\", - \\"description\\": \\"Description 1\\", - \\"__typename\\": \\"ShoppingCartItem\\" -}", + "Missing field '%s' while writing result %o", + "price", + Object { + "__typename": "ShoppingCartItem", + "description": "Description 1", + "id": "1", + "name": "Todo 1", + }, ], ], "results": Array [ @@ -226,10 +230,12 @@ exports[`writing to the store writeResultToStore shape checking should warn when [MockFunction] { "calls": Array [ Array [ - "Missing field 'description' while writing result { - \\"id\\": \\"1\\", - \\"name\\": \\"Todo 1\\" -}", + "Missing field '%s' while writing result %o", + "description", + Object { + "id": "1", + "name": "Todo 1", + }, ], ], "results": Array [ @@ -245,10 +251,12 @@ exports[`writing to the store writeResultToStore shape checking should write the [MockFunction] { "calls": Array [ Array [ - "Missing field 'description' while writing result { - \\"id\\": \\"1\\", - \\"name\\": \\"Todo 1\\" -}", + "Missing field '%s' while writing result %o", + "description", + Object { + "id": "1", + "name": "Todo 1", + }, ], ], "results": Array [ diff --git a/src/cache/inmemory/__tests__/entityStore.ts b/src/cache/inmemory/__tests__/entityStore.ts index 8cde06d4510..a6bb2edb893 100644 --- a/src/cache/inmemory/__tests__/entityStore.ts +++ b/src/cache/inmemory/__tests__/entityStore.ts @@ -8,6 +8,8 @@ import { Cache } from '../../core/types/Cache'; import { Reference, makeReference, isReference, StoreValue } from '../../../utilities/graphql/storeUtils'; import { MissingFieldError } from '../..'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { stringifyForDisplay } from '../../../utilities'; +import { InvariantError } from '../../../utilities/globals'; describe('EntityStore', () => { it('should support result caching if so configured', () => { @@ -1782,11 +1784,11 @@ describe('EntityStore', () => { try { expect(cache.identify(ABCs)).toBeUndefined(); expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toHaveBeenCalledWith( - new Error(`Missing field 'b' while extracting keyFields from ${ - JSON.stringify(ABCs) - }`), - ); + expect(consoleWarnSpy).toHaveBeenCalledWith(new InvariantError( + `Missing field 'b' while extracting keyFields from ${ + stringifyForDisplay(ABCs, 2) + }`, + )); } finally { consoleWarnSpy.mockRestore(); } diff --git a/src/cache/inmemory/__tests__/fragmentMatcher.ts b/src/cache/inmemory/__tests__/fragmentMatcher.ts index 75d718db08b..c28b9df062f 100644 --- a/src/cache/inmemory/__tests__/fragmentMatcher.ts +++ b/src/cache/inmemory/__tests__/fragmentMatcher.ts @@ -234,8 +234,8 @@ describe("policies.fragmentMatches", () => { beforeEach(() => { warnings.length = 0; - console.warn = function (message: any) { - warnings.push(message); + console.warn = function (...args: any) { + warnings.push(args); }; }); @@ -384,9 +384,9 @@ describe("policies.fragmentMatches", () => { }); expect(warnings).toEqual([ - "Inferring subtype E of supertype C", - "Inferring subtype F of supertype C", - "Inferring subtype G of supertype C", + ["Inferring subtype %s of supertype %s", "E", "C"], + ["Inferring subtype %s of supertype %s", "F", "C"], + ["Inferring subtype %s of supertype %s", "G", "C"], // Note that TooLong is not inferred here. ]); diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts index 11ac14dcb16..87fb6e17223 100644 --- a/src/cache/inmemory/__tests__/policies.ts +++ b/src/cache/inmemory/__tests__/policies.ts @@ -4,7 +4,7 @@ import { InMemoryCache } from "../inMemoryCache"; import { ReactiveVar, makeVar } from "../reactiveVars"; import { Reference, StoreObject, ApolloClient, NetworkStatus, TypedDocumentNode, DocumentNode } from "../../../core"; import { MissingFieldError } from "../.."; -import { relayStylePagination } from "../../../utilities"; +import { relayStylePagination, stringifyForDisplay } from "../../../utilities"; import { FieldPolicy, StorageType } from "../policies"; import { itAsync, @@ -443,8 +443,9 @@ describe("type policies", function () { }, }); }).toThrowError( - `Missing field 'year' while extracting keyFields from ${JSON.stringify( - theInformationBookData + `Missing field 'year' while extracting keyFields from ${stringifyForDisplay( + theInformationBookData, + 2 )}`, ); }); diff --git a/src/cache/inmemory/key-extractor.ts b/src/cache/inmemory/key-extractor.ts index 58c56777782..722a59812ce 100644 --- a/src/cache/inmemory/key-extractor.ts +++ b/src/cache/inmemory/key-extractor.ts @@ -73,9 +73,9 @@ export function keyFieldsFnFromSpecifier( invariant( extracted !== void 0, - `Missing field '${schemaKeyPath.join('.')}' while extracting keyFields from ${ - JSON.stringify(object) - }`, + `Missing field '%s' while extracting keyFields from %s`, + schemaKeyPath.join('.'), + object, ); return extracted; diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index 9e12b3c23f0..7edb5c0d7fb 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError, __DEV__ } from '../../utilities/globals'; +import { invariant, newInvariantError, __DEV__ } from '../../utilities/globals'; import type { InlineFragmentNode, @@ -513,7 +513,7 @@ export class Policies { const rootId = "ROOT_" + which.toUpperCase(); const old = this.rootTypenamesById[rootId]; if (typename !== old) { - invariant(!old || old === which, `Cannot change root ${which} __typename more than once`); + invariant(!old || old === which, `Cannot change root %s __typename more than once`, which); // First, delete any old __typename associated with this rootId from // rootIdsByTypename. if (old) delete this.rootIdsByTypename[old]; @@ -687,7 +687,7 @@ export class Policies { if (supertypeSet.has(supertype)) { if (!typenameSupertypeSet.has(supertype)) { if (checkingFuzzySubtypes) { - invariant.warn(`Inferring subtype ${typename} of supertype ${supertype}`); + invariant.warn(`Inferring subtype %s of supertype %s`, typename, supertype); } // Record positive results for faster future lookup. // Unfortunately, we cannot safely cache negative results, @@ -974,9 +974,7 @@ export function normalizeReadFieldOptions( } if (__DEV__ && options.from === void 0) { - invariant.warn(`Undefined 'from' passed to readField with arguments ${ - stringifyForDisplay(Array.from(readFieldArgs)) - }`); + invariant.warn(`Undefined 'from' passed to readField with arguments %s`, stringifyForDisplay(Array.from(readFieldArgs))); } if (void 0 === options.variables) { @@ -991,7 +989,7 @@ function makeMergeObjectsFunction( ): MergeObjectsFunction { return function mergeObjects(existing, incoming) { if (isArray(existing) || isArray(incoming)) { - throw new InvariantError("Cannot automatically merge arrays"); + throw newInvariantError("Cannot automatically merge arrays"); } // These dynamic checks are necessary because the parameters of a diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index 1ba72a639ea..701d6fd4607 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError, __DEV__ } from '../../utilities/globals'; +import { invariant, newInvariantError, __DEV__ } from '../../utilities/globals'; import type { DocumentNode, @@ -412,7 +412,7 @@ export class StoreReader { ); if (!fragment && selection.kind === Kind.FRAGMENT_SPREAD) { - throw new InvariantError(`No fragment named ${selection.name.value}`); + throw newInvariantError(`No fragment named %s`, selection.name.value); } if (fragment && policies.fragmentMatches(fragment, typename)) { @@ -521,9 +521,9 @@ function assertSelectionSetForIdValue( if (isNonNullObject(value)) { invariant( !isReference(value), - `Missing selection set for object of type ${ - getTypenameFromStoreObject(store, value) - } returned for query field ${field.name.value}`, + `Missing selection set for object of type %s returned for query field %s`, + getTypenameFromStoreObject(store, value), + field.name.value ); Object.values(value).forEach(workSet.add, workSet); } diff --git a/src/cache/inmemory/writeToStore.ts b/src/cache/inmemory/writeToStore.ts index 9076e91b7fd..414d81ee62e 100644 --- a/src/cache/inmemory/writeToStore.ts +++ b/src/cache/inmemory/writeToStore.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError, __DEV__ } from '../../utilities/globals'; +import { invariant, newInvariantError, __DEV__ } from '../../utilities/globals'; import { equal } from '@wry/equality'; import { Trie } from '@wry/trie'; import type { @@ -150,7 +150,7 @@ export class StoreWriter { }); if (!isReference(ref)) { - throw new InvariantError(`Could not identify object ${JSON.stringify(result)}`); + throw newInvariantError(`Could not identify object %s`, result); } // So far, the store has not been modified, so now it's time to process @@ -359,11 +359,7 @@ export class StoreWriter { // not be cause for alarm. !policies.getReadFunction(typename, field.name.value) ) { - invariant.error(`Missing field '${ - resultKeyNameFromField(field) - }' while writing result ${ - JSON.stringify(result, null, 2) - }`.substring(0, 1000)); + invariant.error(`Missing field '%s' while writing result %o`, resultKeyNameFromField(field), result); } }); @@ -570,7 +566,7 @@ export class StoreWriter { ); if (!fragment && selection.kind === Kind.FRAGMENT_SPREAD) { - throw new InvariantError(`No fragment named ${selection.name.value}`); + throw newInvariantError(`No fragment named %s`, selection.name.value); } if (fragment && @@ -815,23 +811,25 @@ function warnAboutDataLoss( } invariant.warn( -`Cache data may be lost when replacing the ${fieldName} field of a ${parentType} object. +`Cache data may be lost when replacing the %s field of a %s object. -To address this problem (which is not a bug in Apollo Client), ${ - childTypenames.length - ? "either ensure all objects of type " + - childTypenames.join(" and ") + " have an ID or a custom merge function, or " - : "" -}define a custom merge function for the ${ - typeDotName -} field, so InMemoryCache can safely merge these objects: +To address this problem (which is not a bug in Apollo Client), %sdefine a custom merge function for the %s field, so InMemoryCache can safely merge these objects: - existing: ${JSON.stringify(existing).slice(0, 1000)} - incoming: ${JSON.stringify(incoming).slice(0, 1000)} + existing: %s + incoming: %s For more information about these options, please refer to the documentation: * Ensuring entity objects have IDs: https://go.apollo.dev/c/generating-unique-identifiers * Defining custom merge functions: https://go.apollo.dev/c/merging-non-normalized-objects -`); +`, + fieldName, + parentType, + childTypenames.length + ? "either ensure all objects of type " + childTypenames.join(" and ") + " have an ID or a custom merge function, or " + : "", + typeDotName, + existing, + incoming +); } diff --git a/src/config/jest/setup.ts b/src/config/jest/setup.ts index a0261ed53c0..179023e7dcb 100644 --- a/src/config/jest/setup.ts +++ b/src/config/jest/setup.ts @@ -1,8 +1,11 @@ import gql from 'graphql-tag'; import '@testing-library/jest-dom'; +import { loadErrorMessageHandler } from '../../dev/loadErrorMessageHandler'; import '../../testing/matchers'; // Turn off warnings for repeated fragment names gql.disableFragmentWarnings(); process.on('unhandledRejection', () => {}); + +loadErrorMessageHandler(); diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 42df9c63f25..ac1a2dbfb29 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError, __DEV__ } from '../utilities/globals'; +import { invariant, newInvariantError, __DEV__ } from '../utilities/globals'; import type { ExecutionResult, DocumentNode } from 'graphql'; @@ -131,7 +131,7 @@ export class ApolloClient implements DataProxy { */ constructor(options: ApolloClientOptions) { if (!options.cache) { - throw new InvariantError( + throw newInvariantError( "To initialize Apollo Client, you must specify a 'cache' property " + "in the options object. \n" + "For more information, please visit: https://go.apollo.dev/c/docs" @@ -220,7 +220,7 @@ export class ApolloClient implements DataProxy { if (url) { invariant.log( "Download the Apollo DevTools for a better development " + - "experience: " + url + "experience: %s", url ); } } @@ -583,7 +583,7 @@ export class ApolloClient implements DataProxy { // result.queries and result.results instead, you shouldn't have to worry // about preventing uncaught rejections for the Promise.all result. result.catch(error => { - invariant.debug(`In client.refetchQueries, Promise.all promise rejected with error ${error}`); + invariant.debug(`In client.refetchQueries, Promise.all promise rejected with error %o`, error); }); return result; diff --git a/src/core/LocalState.ts b/src/core/LocalState.ts index 5dfe7d7312c..a9e026e0e29 100644 --- a/src/core/LocalState.ts +++ b/src/core/LocalState.ts @@ -343,7 +343,7 @@ export class LocalState { } else { // This is a named fragment. fragment = fragmentMap[selection.name.value]; - invariant(fragment, `No fragment named ${selection.name.value}`); + invariant(fragment, `No fragment named %s`, selection.name.value); } if (fragment && fragment.typeCondition) { @@ -508,7 +508,7 @@ export class LocalState { }, FragmentSpread(spread: FragmentSpreadNode, _, __, ___, ancestors) { const fragment = fragmentMap[spread.name.value]; - invariant(fragment, `No fragment named ${spread.name.value}`); + invariant(fragment, `No fragment named %s`, spread.name.value); const fragmentSelections = collectByDefinition(fragment); if (fragmentSelections.size > 0) { diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 94484f8468c..4a14d6432aa 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -391,12 +391,11 @@ export class ObservableQuery< const queryDef = getQueryDefinition(this.query); const vars = queryDef.variableDefinitions; if (!vars || !vars.some(v => v.variable.name.value === "variables")) { - invariant.warn(`Called refetch(${ - JSON.stringify(variables) - }) for query ${ - queryDef.name?.value || JSON.stringify(queryDef) - }, which does not declare a $variables variable. -Did you mean to call refetch(variables) instead of refetch({ variables })?`); + invariant.warn(`Called refetch(%o) for query %o, which does not declare a $variables variable. +Did you mean to call refetch(variables) instead of refetch({ variables })?`, + variables, + queryDef.name?.value || queryDef + ); } } @@ -1033,8 +1032,6 @@ export function logMissingFieldErrors( missing: MissingFieldError[] | MissingTree | undefined, ) { if (__DEV__ && missing) { - invariant.debug(`Missing cache result fields: ${ - JSON.stringify(missing) - }`, missing); + invariant.debug(`Missing cache result fields: %o`, missing); } } diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index b7c55833f70..491fcc3b63c 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError, __DEV__ } from '../utilities/globals'; +import { invariant, newInvariantError, __DEV__ } from '../utilities/globals'; import type { DocumentNode } from 'graphql'; // TODO(brian): A hack until this issue is resolved (https://github.com/graphql/graphql-js/issues/3356) @@ -181,7 +181,7 @@ export class QueryManager { }); this.cancelPendingFetches( - new InvariantError('QueryManager stopped while query was in flight'), + newInvariantError('QueryManager stopped while query was in flight'), ); } @@ -793,7 +793,7 @@ export class QueryManager { // depend on values that previously existed in the data portion of the // store. So, we cancel the promises and observers that we have issued // so far and not yet resolved (in the case of queries). - this.cancelPendingFetches(new InvariantError( + this.cancelPendingFetches(newInvariantError( 'Store reset while query was in flight (not completed in link chain)', )); @@ -892,11 +892,7 @@ export class QueryManager { if (__DEV__ && queryNamesAndDocs.size) { queryNamesAndDocs.forEach((included, nameOrDoc) => { if (!included) { - invariant.warn(`Unknown query ${ - typeof nameOrDoc === "string" ? "named " : "" - }${ - JSON.stringify(nameOrDoc, null, 2) - } requested in refetchQueries options.include array`); + invariant.warn(typeof nameOrDoc === "string" ? `Unknown query named "%s" requested in refetchQueries options.include array` : `Unknown query %s requested in refetchQueries options.include array`, nameOrDoc); } }); } diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index 45d1c80b136..c20b8afe13d 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -1474,9 +1474,11 @@ describe("ObservableQuery", () => { expect(consoleWarnSpy).toHaveBeenCalledTimes(1); expect(consoleWarnSpy).toHaveBeenCalledWith( [ - 'Called refetch({"variables":["d","e"]}) for query QueryWithoutVariables, which does not declare a $variables variable.', + 'Called refetch(%o) for query %o, which does not declare a $variables variable.', "Did you mean to call refetch(variables) instead of refetch({ variables })?", - ].join("\n") + ].join("\n"), + {"variables": ["d", "e"]}, + "QueryWithoutVariables" ); consoleWarnSpy.mockRestore(); @@ -1581,9 +1583,11 @@ describe("ObservableQuery", () => { expect(consoleWarnSpy).toHaveBeenCalledTimes(1); expect(consoleWarnSpy).toHaveBeenCalledWith( [ - 'Called refetch({"variables":{"vars":["d","e"]}}) for query QueryWithVarsVar, which does not declare a $variables variable.', + 'Called refetch(%o) for query %o, which does not declare a $variables variable.', "Did you mean to call refetch(variables) instead of refetch({ variables })?", - ].join("\n") + ].join("\n"), + {"variables":{"vars":["d","e"]}}, + "QueryWithVarsVar" ); consoleWarnSpy.mockRestore(); diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index abc593d7e2d..4bb9c9f68ff 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -4847,7 +4847,7 @@ describe('QueryManager', () => { result => { expect(result.data).toEqual(secondReqData); expect(consoleWarnSpy).toHaveBeenLastCalledWith( - 'Unknown query named "fakeQuery" requested in refetchQueries options.include array' + 'Unknown query named "%s" requested in refetchQueries options.include array', "fakeQuery" ); }, ).then(resolve, reject); @@ -4914,7 +4914,7 @@ describe('QueryManager', () => { }); }).then(() => { expect(consoleWarnSpy).toHaveBeenLastCalledWith( - 'Unknown query named "getAuthors" requested in refetchQueries options.include array' + 'Unknown query named "%s" requested in refetchQueries options.include array', "getAuthors" ); }).then(resolve, reject); }); diff --git a/src/dev/index.ts b/src/dev/index.ts new file mode 100644 index 00000000000..d9472d58c7e --- /dev/null +++ b/src/dev/index.ts @@ -0,0 +1,3 @@ +export { loadDevMessages } from './loadDevMessages'; +export { loadErrorMessageHandler } from './loadErrorMessageHandler'; +export { loadErrorMessages } from './loadErrorMessages'; diff --git a/src/dev/loadDevMessages.ts b/src/dev/loadDevMessages.ts new file mode 100644 index 00000000000..fdb9d3f082d --- /dev/null +++ b/src/dev/loadDevMessages.ts @@ -0,0 +1,6 @@ +import { devDebug, devError, devLog, devWarn } from '../invariantErrorCodes'; +import { loadErrorMessageHandler } from './loadErrorMessageHandler'; + +export function loadDevMessages() { + loadErrorMessageHandler(devDebug, devError, devLog, devWarn); +} diff --git a/src/dev/loadErrorMessageHandler.ts b/src/dev/loadErrorMessageHandler.ts new file mode 100644 index 00000000000..28fafa48a39 --- /dev/null +++ b/src/dev/loadErrorMessageHandler.ts @@ -0,0 +1,27 @@ +import type { ErrorCodes } from '../invariantErrorCodes'; +import { global } from '../utilities/globals'; +import { ApolloErrorMessageHandler } from '../utilities/globals/invariantWrappers'; + +export function loadErrorMessageHandler(...errorCodes: ErrorCodes[]) { + if (!global[ApolloErrorMessageHandler]) { + global[ApolloErrorMessageHandler] = handler as typeof handler & ErrorCodes; + } + + for (const codes of errorCodes) { + Object.assign(global[ApolloErrorMessageHandler], codes); + } + + return global[ApolloErrorMessageHandler]; + + function handler(message: string | number, args: unknown[]) { + if (typeof message === 'number') { + const definition = global[ApolloErrorMessageHandler]![message]; + if (!message || !definition.message) return; + message = definition.message; + } + return args.reduce( + (msg, arg) => msg.replace('%s', String(arg)), + String(message) + ); + } +} diff --git a/src/dev/loadErrorMessages.ts b/src/dev/loadErrorMessages.ts new file mode 100644 index 00000000000..a5b59984941 --- /dev/null +++ b/src/dev/loadErrorMessages.ts @@ -0,0 +1,6 @@ +import { errorCodes } from '../invariantErrorCodes'; +import { loadErrorMessageHandler } from './loadErrorMessageHandler'; + +export function loadErrorMessages() { + loadErrorMessageHandler(errorCodes); +} diff --git a/src/invariantErrorCodes.ts b/src/invariantErrorCodes.ts new file mode 100644 index 00000000000..f1424662d56 --- /dev/null +++ b/src/invariantErrorCodes.ts @@ -0,0 +1,9 @@ +export interface ErrorCodes { + [key: number]: { file: string, condition?: string, message?: string } +} + +export const errorCodes: ErrorCodes = {}; +export const devDebug: ErrorCodes = {}; +export const devLog: ErrorCodes = {}; +export const devWarn: ErrorCodes = {}; +export const devError: ErrorCodes = {}; diff --git a/src/link/batch-http/__tests__/batchHttpLink.ts b/src/link/batch-http/__tests__/batchHttpLink.ts index 112b877ad6f..88a99c1fbaf 100644 --- a/src/link/batch-http/__tests__/batchHttpLink.ts +++ b/src/link/batch-http/__tests__/batchHttpLink.ts @@ -298,7 +298,7 @@ describe('SharedHttpTest', () => { it('raises warning if called with concat', () => { const link = createHttpLink(); const _warn = console.warn; - console.warn = (warning: any) => expect(warning['message']).toBeDefined(); + console.warn = (...args: any) => expect(args).toEqual(["You are calling concat on a terminating link, which will have no effect %o", link]); expect(link.concat((operation, forward) => forward(operation))).toEqual( link, ); diff --git a/src/link/batch/__tests__/batchLink.ts b/src/link/batch/__tests__/batchLink.ts index fe19048ef92..25659f5a0ea 100644 --- a/src/link/batch/__tests__/batchLink.ts +++ b/src/link/batch/__tests__/batchLink.ts @@ -793,9 +793,9 @@ describe('BatchLink', () => { }); const link_no_op = new BatchLink({ batchHandler: () => Observable.of() }); const _warn = console.warn; - console.warn = (warning: any) => { + console.warn = (...args: any) => { calls++; - expect(warning.message).toBeDefined(); + expect(args).toEqual(["You are calling concat on a terminating link, which will have no effect %o", expect.any(BatchLink)]); }; expect( link_one_op.concat((operation, forward) => forward(operation)), diff --git a/src/link/core/ApolloLink.ts b/src/link/core/ApolloLink.ts index d3c26ff2164..9b26e56f8ee 100644 --- a/src/link/core/ApolloLink.ts +++ b/src/link/core/ApolloLink.ts @@ -1,4 +1,4 @@ -import { InvariantError, invariant } from '../../utilities/globals'; +import { newInvariantError, invariant } from '../../utilities/globals'; import type { Observer } from '../../utilities'; import { Observable } from '../../utilities'; @@ -27,14 +27,6 @@ function isTerminating(link: ApolloLink): boolean { return link.request.length <= 1; } -class LinkError extends Error { - public link?: ApolloLink; - constructor(message?: string, link?: ApolloLink) { - super(message); - this.link = link; - } -} - export class ApolloLink { public static empty(): ApolloLink { return new ApolloLink(() => Observable.of()); @@ -89,10 +81,8 @@ export class ApolloLink { const firstLink = toLink(first); if (isTerminating(firstLink)) { invariant.warn( - new LinkError( - `You are calling concat on a terminating link, which will have no effect`, + `You are calling concat on a terminating link, which will have no effect %o`, firstLink, - ), ); return firstLink; } @@ -139,7 +129,7 @@ export class ApolloLink { operation: Operation, forward?: NextLink, ): Observable | null { - throw new InvariantError('request is not implemented'); + throw newInvariantError('request is not implemented'); } protected onError( diff --git a/src/link/core/__tests__/ApolloLink.ts b/src/link/core/__tests__/ApolloLink.ts index 369873a951e..0f00877a59a 100644 --- a/src/link/core/__tests__/ApolloLink.ts +++ b/src/link/core/__tests__/ApolloLink.ts @@ -921,9 +921,9 @@ describe('ApolloClient', () => { describe('Terminating links', () => { const _warn = console.warn; - const warningStub = jest.fn(warning => { - expect(warning.message).toBe( - `You are calling concat on a terminating link, which will have no effect`, + const warningStub = jest.fn((warning, link) => { + expect(warning).toBe( + `You are calling concat on a terminating link, which will have no effect %o`, ); }); const data = { @@ -1017,7 +1017,7 @@ describe('ApolloClient', () => { link, ); expect(warningStub).toHaveBeenCalledTimes(1); - expect(warningStub.mock.calls[0][0].link).toEqual(link); + expect(warningStub.mock.calls[0][1]).toEqual(link); }); it('should warn if attempting to concat to a terminating Link', () => { @@ -1026,7 +1026,7 @@ describe('ApolloClient', () => { link, ); expect(warningStub).toHaveBeenCalledTimes(1); - expect(warningStub.mock.calls[0][0].link).toEqual(link); + expect(warningStub.mock.calls[0][1]).toEqual(link); }); it('should not warn if attempting concat a terminating Link at end', () => { diff --git a/src/link/http/__tests__/HttpLink.ts b/src/link/http/__tests__/HttpLink.ts index 373a24af229..f0525edc4ca 100644 --- a/src/link/http/__tests__/HttpLink.ts +++ b/src/link/http/__tests__/HttpLink.ts @@ -454,7 +454,7 @@ describe('HttpLink', () => { it('raises warning if called with concat', () => { const link = createHttpLink(); const _warn = console.warn; - console.warn = (warning: any) => expect(warning['message']).toBeDefined(); + console.warn = (...args: any) => expect(args).toEqual(["You are calling concat on a terminating link, which will have no effect %o", link]); expect(link.concat((operation, forward) => forward(operation))).toEqual( link, ); diff --git a/src/link/http/checkFetcher.ts b/src/link/http/checkFetcher.ts index 4a58881a6c0..5bbb672347b 100644 --- a/src/link/http/checkFetcher.ts +++ b/src/link/http/checkFetcher.ts @@ -1,8 +1,8 @@ -import { InvariantError } from '../../utilities/globals'; +import { newInvariantError } from '../../utilities/globals'; export const checkFetcher = (fetcher: WindowOrWorkerGlobalScope['fetch'] | undefined) => { if (!fetcher && typeof fetch === 'undefined') { - throw new InvariantError(` + throw newInvariantError(` "fetch" has not been found globally and no fetcher has been \ configured. To fix this, install a fetch package (like \ https://www.npmjs.com/package/cross-fetch), instantiate the \ diff --git a/src/link/http/serializeFetchParameter.ts b/src/link/http/serializeFetchParameter.ts index 85de32e6a6a..920f3b73592 100644 --- a/src/link/http/serializeFetchParameter.ts +++ b/src/link/http/serializeFetchParameter.ts @@ -1,4 +1,5 @@ -import { InvariantError } from '../../utilities/globals'; +import { newInvariantError } from '../../utilities/globals'; +import type { InvariantError } from '../../utilities/globals'; export type ClientParseError = InvariantError & { parseError: Error; @@ -9,8 +10,10 @@ export const serializeFetchParameter = (p: any, label: string) => { try { serialized = JSON.stringify(p); } catch (e) { - const parseError = new InvariantError( - `Network request failed. ${label} is not serializable: ${e.message}`, + const parseError = newInvariantError( + `Network request failed. %s is not serializable: %s`, + label, + e.message ) as ClientParseError; parseError.parseError = e; throw parseError; diff --git a/src/link/schema/__tests__/schemaLink.ts b/src/link/schema/__tests__/schemaLink.ts index f6334967c84..8d0cca8d4f0 100644 --- a/src/link/schema/__tests__/schemaLink.ts +++ b/src/link/schema/__tests__/schemaLink.ts @@ -29,7 +29,7 @@ describe('SchemaLink', () => { it('raises warning if called with concat', () => { const link = new SchemaLink({ schema }); const _warn = console.warn; - console.warn = (warning: any) => expect(warning['message']).toBeDefined(); + console.warn = (...args) => expect(args).toEqual(["You are calling concat on a terminating link, which will have no effect %o", link]); expect(link.concat((operation, forward) => forward(operation))).toEqual( link, ); diff --git a/src/link/utils/validateOperation.ts b/src/link/utils/validateOperation.ts index e02cceb70a7..3d8542733e1 100644 --- a/src/link/utils/validateOperation.ts +++ b/src/link/utils/validateOperation.ts @@ -1,4 +1,4 @@ -import { InvariantError } from '../../utilities/globals' +import { newInvariantError } from '../../utilities/globals' import type { GraphQLRequest } from '../core'; export function validateOperation(operation: GraphQLRequest): GraphQLRequest { @@ -11,7 +11,7 @@ export function validateOperation(operation: GraphQLRequest): GraphQLRequest { ]; for (let key of Object.keys(operation)) { if (OPERATION_FIELDS.indexOf(key) < 0) { - throw new InvariantError(`illegal argument: ${key}`); + throw newInvariantError(`illegal argument: %s`, key); } } diff --git a/src/react/context/ApolloProvider.tsx b/src/react/context/ApolloProvider.tsx index a38f76ce8cf..aa1bf067164 100644 --- a/src/react/context/ApolloProvider.tsx +++ b/src/react/context/ApolloProvider.tsx @@ -15,7 +15,7 @@ export interface ApolloProviderProps { export const ApolloProvider: React.FC> = ({ client, suspenseCache, - children + children, }) => { const ApolloContext = getApolloContext(); const parentContext = React.useContext(ApolloContext); @@ -24,8 +24,8 @@ export const ApolloProvider: React.FC> = ({ return { ...parentContext, client: client || parentContext.client, - suspenseCache: suspenseCache || parentContext.suspenseCache - } + suspenseCache: suspenseCache || parentContext.suspenseCache, + }; }, [parentContext, client, suspenseCache]); invariant( @@ -35,8 +35,6 @@ export const ApolloProvider: React.FC> = ({ ); return ( - - {children} - + {children} ); }; diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 359f74869cb..5cef1398fa1 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -5016,13 +5016,13 @@ describe('useQuery Hook', () => { expect(errorSpy).toHaveBeenCalled(); expect(errorSpy).toHaveBeenLastCalledWith( - `Missing field 'vin' while writing result ${JSON.stringify({ + `Missing field '%s' while writing result %o`, 'vin', { id: 1, make: "Audi", model: "RS8", vine: "DOLLADOLLABILL", __typename: "Car" - }, null, 2)}` + } ); errorSpy.mockRestore(); }); diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index dbaaa565649..31ef93cb4eb 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -534,8 +534,8 @@ describe('useSubscription Hook', () => { expect(result.current.data).toBe(null); expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy.mock.calls[0][0]).toBe( - "Missing field 'car' while writing result {}", + expect(errorSpy.mock.calls[0]).toStrictEqual( + ["Missing field '%s' while writing result %o", "car", Object.create(null)] ); errorSpy.mockRestore(); }); @@ -600,14 +600,14 @@ describe('useSubscription Hook', () => { expect(result.current.sub3.data).toBe(null); expect(errorSpy).toHaveBeenCalledTimes(3); - expect(errorSpy.mock.calls[0][0]).toBe( - "Missing field 'car' while writing result {}", + expect(errorSpy.mock.calls[0]).toStrictEqual( + ["Missing field '%s' while writing result %o", "car", Object.create(null)] ); - expect(errorSpy.mock.calls[1][0]).toBe( - "Missing field 'car' while writing result {}", + expect(errorSpy.mock.calls[1]).toStrictEqual( + ["Missing field '%s' while writing result %o", "car", Object.create(null)] ); - expect(errorSpy.mock.calls[2][0]).toBe( - "Missing field 'car' while writing result {}", + expect(errorSpy.mock.calls[2]).toStrictEqual( + ["Missing field '%s' while writing result %o", "car", Object.create(null)] ); errorSpy.mockRestore(); }); diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index b9eca1bfe5f..60a11d85982 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -233,7 +233,8 @@ function validateFetchPolicy( invariant( supportedFetchPolicies.includes(fetchPolicy), - `The fetch policy \`${fetchPolicy}\` is not supported with suspense.` + `The fetch policy \`%s\` is not supported with suspense.`, + fetchPolicy ); } diff --git a/src/react/parser/index.ts b/src/react/parser/index.ts index a9417e242bc..def98e4e04c 100644 --- a/src/react/parser/index.ts +++ b/src/react/parser/index.ts @@ -46,9 +46,10 @@ export function parser(document: DocumentNode): IDocumentDefinition { invariant( !!document && !!document.kind, - `Argument of ${document} passed to parser was not a valid GraphQL ` + + `Argument of %s passed to parser was not a valid GraphQL ` + `DocumentNode. You may need to use 'graphql-tag' or another method ` + - `to convert your operation into a document` + `to convert your operation into a document`, + document ); const fragments: DefinitionNode[] = [] @@ -87,9 +88,13 @@ export function parser(document: DocumentNode): IDocumentDefinition { invariant( queries.length + mutations.length + subscriptions.length <= 1, `react-apollo only supports a query, subscription, or a mutation per HOC. ` + - `${document} had ${queries.length} queries, ${subscriptions.length} ` + - `subscriptions and ${mutations.length} mutations. ` + - `You can use 'compose' to join multiple operation types to a component` + `%s had %s queries, %s ` + + `subscriptions and %s mutations. ` + + `You can use 'compose' to join multiple operation types to a component`, + document, + queries.length, + subscriptions.length, + mutations.length ); type = queries.length ? DocumentType.Query : DocumentType.Mutation; @@ -103,9 +108,11 @@ export function parser(document: DocumentNode): IDocumentDefinition { invariant( definitions.length === 1, - `react-apollo only supports one definition per HOC. ${document} had ` + - `${definitions.length} definitions. ` + - `You can use 'compose' to join multiple operation types to a component` + `react-apollo only supports one definition per HOC. %s had ` + + `%s definitions. ` + + `You can use 'compose' to join multiple operation types to a component`, + document, + definitions.length ); const definition = definitions[0] as OperationDefinitionNode; @@ -128,8 +135,11 @@ export function verifyDocumentType(document: DocumentNode, type: DocumentType) { const usedOperationName = operationName(operation.type); invariant( operation.type === type, - `Running a ${requiredOperationName} requires a graphql ` + - `${requiredOperationName}, but a ${usedOperationName} was used instead.` + `Running a %s requires a graphql ` + + `%s, but a %s was used instead.`, + requiredOperationName, + requiredOperationName, + usedOperationName ); } diff --git a/src/utilities/common/stringifyForDisplay.ts b/src/utilities/common/stringifyForDisplay.ts index bce4d50b6ee..3499d6ca8f7 100644 --- a/src/utilities/common/stringifyForDisplay.ts +++ b/src/utilities/common/stringifyForDisplay.ts @@ -1,8 +1,8 @@ import { makeUniqueId } from "./makeUniqueId"; -export function stringifyForDisplay(value: any): string { +export function stringifyForDisplay(value: any, space = 0): string { const undefId = makeUniqueId("stringifyForDisplay"); return JSON.stringify(value, (key, value) => { return value === void 0 ? undefId : value; - }).split(JSON.stringify(undefId)).join(""); + }, space).split(JSON.stringify(undefId)).join(""); } diff --git a/src/utilities/globals/index.ts b/src/utilities/globals/index.ts index acfe07a6136..20375b544f4 100644 --- a/src/utilities/globals/index.ts +++ b/src/utilities/globals/index.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError } from "ts-invariant"; +import { invariant, newInvariantError, InvariantError } from "./invariantWrappers"; // Just in case the graphql package switches from process.env.NODE_ENV to // __DEV__, make sure __DEV__ is polyfilled before importing graphql. @@ -16,4 +16,4 @@ removeTemporaryGlobals(); export { maybe } from "./maybe"; export { default as global } from "./global"; -export { invariant, InvariantError } +export { invariant, newInvariantError, InvariantError } diff --git a/src/utilities/globals/invariantWrappers.ts b/src/utilities/globals/invariantWrappers.ts new file mode 100644 index 00000000000..f3dd751a808 --- /dev/null +++ b/src/utilities/globals/invariantWrappers.ts @@ -0,0 +1,128 @@ +import { invariant as originalInvariant, InvariantError } from 'ts-invariant'; +import { version } from '../../version'; +import global from './global'; +import type { ErrorCodes } from '../../invariantErrorCodes'; +import { stringifyForDisplay } from '../common/stringifyForDisplay'; + +function wrap(fn: (msg?: string, ...args: any[]) => void) { + return function (message: string | number, ...args: any[]) { + fn(typeof message === 'number' ? getErrorMsg(message) : message, ...args); + }; +} + +type LogFunction = { + /** + * Logs a `$level` message if the user used `ts-invariant`'s `setVerbosity` to set + * a verbosity level of `$level` or lower. (defaults to `"log"`). + * + * The user will either be presented with a link to the documentation for the message, + * or they can use the `loadDevMessages` to add the message strings to the bundle. + * The documentation will display the message without argument substitution. + * Instead, the arguments will be printed on the console after the link. + * + * `message` can only be a string, a concatenation of strings, or a ternary statement + * that results in a string. This will be enforced on build, where the message will + * be replaced with a message number. + * + * String substitutions like %s, %o, %d or %f are supported. + */ + (message?: any, ...optionalParams: unknown[]): void; +}; + +type WrappedInvariant = { + /** + * Throws and InvariantError with the given message if the condition is false. + * + * `message` can only be a string, a concatenation of strings, or a ternary statement + * that results in a string. This will be enforced on build, where the message will + * be replaced with a message number. + * + * The user will either be presented with a link to the documentation for the message, + * or they can use the `loadErrorMessages` to add the message strings to the bundle. + * The documentation will display the message with the arguments substituted. + * + * String substitutions with %s are supported and will also return + * pretty-stringified objects. + * Excess `optionalParams` will be swallowed. + */ + ( + condition: any, + message?: string | number, + ...optionalParams: unknown[] + ): asserts condition; + + debug: LogFunction; + log: LogFunction; + warn: LogFunction; + error: LogFunction; +}; +const invariant: WrappedInvariant = Object.assign( + function invariant( + condition: any, + message?: string | number, + ...args: unknown[] + ): asserts condition { + if (!condition) { + originalInvariant(condition, getErrorMsg(message, args)); + } + }, + { + debug: wrap(originalInvariant.debug), + log: wrap(originalInvariant.log), + warn: wrap(originalInvariant.warn), + error: wrap(originalInvariant.error), + } +); + +/** + * Returns an InvariantError. + * + * `message` can only be a string, a concatenation of strings, or a ternary statement + * that results in a string. This will be enforced on build, where the message will + * be replaced with a message number. + * String substitutions with %s are supported and will also return + * pretty-stringified objects. + * Excess `optionalParams` will be swallowed. + */ +function newInvariantError( + message?: string | number, + ...optionalParams: unknown[] +) { + return new InvariantError(getErrorMsg(message, optionalParams)); +} + +const ApolloErrorMessageHandler = Symbol.for( + 'ApolloErrorMessageHandler_' + version +); +declare global { + interface Window { + [ApolloErrorMessageHandler]?: { + (message: string | number, args: unknown[]): string | undefined; + } & ErrorCodes; + } +} + +function getErrorMsg(message?: string | number, messageArgs: unknown[] = []) { + if (!message) return; + const args = messageArgs.map((arg) => + typeof arg == 'string' ? arg : stringifyForDisplay(arg, 2).slice(0, 1000) + ); + return ( + (global[ApolloErrorMessageHandler] && + global[ApolloErrorMessageHandler](message, args)) || + `An error occured! For more details, see the full error text at https://go.apollo.dev/c/err#${encodeURIComponent( + JSON.stringify({ + version, + message, + args, + }) + )}` + ); +} + +export { + invariant, + InvariantError, + newInvariantError, + ApolloErrorMessageHandler, +}; diff --git a/src/utilities/graphql/directives.ts b/src/utilities/graphql/directives.ts index 9072e54aa73..e3eaaf86340 100644 --- a/src/utilities/graphql/directives.ts +++ b/src/utilities/graphql/directives.ts @@ -35,7 +35,8 @@ export function shouldInclude( evaledValue = variables && variables[(ifArgument.value as VariableNode).name.value]; invariant( evaledValue !== void 0, - `Invalid variable referenced in @${directive.name.value} directive.`, + `Invalid variable referenced in @%s directive.`, + directive.name.value ); } else { evaledValue = (ifArgument.value as BooleanValueNode).value; @@ -117,13 +118,15 @@ export function getInclusionDirectives( invariant( directiveArguments && directiveArguments.length === 1, - `Incorrect number of arguments for the @${directiveName} directive.`, + `Incorrect number of arguments for the @%s directive.`, + directiveName ); const ifArgument = directiveArguments![0]; invariant( ifArgument.name && ifArgument.name.value === 'if', - `Invalid argument for the @${directiveName} directive.`, + `Invalid argument for the @%s directive.`, + directiveName ); const ifValue: ValueNode = ifArgument.value; @@ -132,7 +135,8 @@ export function getInclusionDirectives( invariant( ifValue && (ifValue.kind === 'Variable' || ifValue.kind === 'BooleanValue'), - `Argument for the @${directiveName} directive must be a variable or a boolean value.`, + `Argument for the @%s directive must be a variable or a boolean value.`, + directiveName ); result.push({ directive, ifArgument }); diff --git a/src/utilities/graphql/fragments.ts b/src/utilities/graphql/fragments.ts index cab54b25873..253497239fe 100644 --- a/src/utilities/graphql/fragments.ts +++ b/src/utilities/graphql/fragments.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError } from '../globals'; +import { invariant, newInvariantError } from '../globals'; import type { DocumentNode, @@ -46,11 +46,11 @@ export function getFragmentQueryDocument( // Throw an error if we encounter an operation definition because we will // define our own operation definition later on. if (definition.kind === 'OperationDefinition') { - throw new InvariantError( - `Found a ${definition.operation} operation${ - definition.name ? ` named '${definition.name.value}'` : '' - }. ` + + throw newInvariantError( + `Found a %s operation%s. ` + 'No operations are allowed when using a fragment as a query. Only fragments are allowed.', + definition.operation, + definition.name ? ` named '${definition.name.value}'` : '' ); } // Add our definition to the fragments array if it is a fragment @@ -65,9 +65,8 @@ export function getFragmentQueryDocument( if (typeof actualFragmentName === 'undefined') { invariant( fragments.length === 1, - `Found ${ - fragments.length - } fragments. \`fragmentName\` must be provided when there is not exactly 1 fragment.`, + `Found %s fragments. \`fragmentName\` must be provided when there is not exactly 1 fragment.`, + fragments.length ); actualFragmentName = fragments[0].name.value; } @@ -136,7 +135,7 @@ export function getFragmentFromSelection( return fragmentMap(fragmentName); } const fragment = fragmentMap && fragmentMap[fragmentName]; - invariant(fragment, `No fragment named ${fragmentName}`); + invariant(fragment, `No fragment named %s`, fragmentName); return fragment || null; } default: diff --git a/src/utilities/graphql/getFromAST.ts b/src/utilities/graphql/getFromAST.ts index 57ef45428bb..4701a1144e5 100644 --- a/src/utilities/graphql/getFromAST.ts +++ b/src/utilities/graphql/getFromAST.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError } from '../globals'; +import { invariant, newInvariantError } from '../globals'; import type { DocumentNode, @@ -25,10 +25,9 @@ string in a "gql" tag? http://docs.apollostack.com/apollo-client/core.html#gql`, .filter(d => d.kind !== 'FragmentDefinition') .map(definition => { if (definition.kind !== 'OperationDefinition') { - throw new InvariantError( - `Schema type definitions not allowed in queries. Found: "${ - definition.kind - }"`, + throw newInvariantError( + `Schema type definitions not allowed in queries. Found: "%s"`, + definition.kind ); } return definition; @@ -36,7 +35,8 @@ string in a "gql" tag? http://docs.apollostack.com/apollo-client/core.html#gql`, invariant( operations.length <= 1, - `Ambiguous GraphQL document: contains ${operations.length} operations`, + `Ambiguous GraphQL document: contains %s operations`, + operations.length ); return doc; @@ -142,7 +142,7 @@ export function getMainDefinition( return fragmentDefinition; } - throw new InvariantError( + throw newInvariantError( 'Expected a parsed GraphQL query with a query, mutation, subscription, or a fragment.', ); } diff --git a/src/utilities/graphql/storeUtils.ts b/src/utilities/graphql/storeUtils.ts index ad598944c9b..9271257f7c5 100644 --- a/src/utilities/graphql/storeUtils.ts +++ b/src/utilities/graphql/storeUtils.ts @@ -1,4 +1,4 @@ -import { InvariantError } from '../globals'; +import { newInvariantError } from '../globals'; import type { DirectiveNode, @@ -132,10 +132,11 @@ export function valueToObjectRepresentation( } else if (isNullValue(value)) { argObj[name.value] = null; } else { - throw new InvariantError( - `The inline argument "${name.value}" of kind "${(value as any).kind}"` + + throw newInvariantError( + `The inline argument "%s" of kind "%s"` + 'is not supported. Use variables instead of inline arguments to ' + 'overcome this limitation.', + name.value, (value as any).kind ); } }