diff --git a/.changeset/lazy-teachers-sell.md b/.changeset/lazy-teachers-sell.md new file mode 100644 index 00000000000..bac8fb053d8 --- /dev/null +++ b/.changeset/lazy-teachers-sell.md @@ -0,0 +1,5 @@ +--- +'@apollo/client': minor +--- + +Simplify `__DEV__` polyfill to use imports instead of global scope diff --git a/config/bundlesize.ts b/config/bundlesize.ts index 86655efd3c9..3794f16ab42 100644 --- a/config/bundlesize.ts +++ b/config/bundlesize.ts @@ -3,7 +3,7 @@ import { join } from "path"; import { gzipSync } from "zlib"; import bytes from "bytes"; -const gzipBundleByteLengthLimit = bytes("33.58KB"); +const gzipBundleByteLengthLimit = bytes("33.7KB"); const minFile = join("dist", "apollo-client.min.cjs"); const minPath = join(__dirname, "..", minFile); const gzipByteLen = gzipSync(readFileSync(minPath)).byteLength; diff --git a/config/distSpotFixes.ts b/config/distSpotFixes.ts deleted file mode 100644 index 41b8afebd79..00000000000 --- a/config/distSpotFixes.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as fs from "fs"; -import { EOL } from "os"; -import { distDir } from './helpers'; - -export function applyDistSpotFixes() { - sanitizeDEV(); -} - -function sanitizeDEV() { - const globalDTsPath = `${distDir}/utilities/globals/global.d.ts`; - const globalDTs = fs.readFileSync(globalDTsPath, "utf8"); - // The global.d.ts types are useful within the @apollo/client codebase to - // provide a type for the global __DEV__ constant, but actually shipping that - // declaration as a globally-declared type runs too much risk of conflict with - // other __DEV__ declarations attempting to achieve the same thing, most - // notably the one in @types/react-native/index.d.ts. We preserve the default - // export so that index.d.ts can remain unchanged, but otherwise we remove all - // traces of __DEV__ from global.d.ts. - if (/__DEV__/.test(globalDTs)) { - fs.writeFileSync(globalDTsPath, [ - "declare const _default: typeof globalThis;", - "export default _default;", - ].join(EOL)); - } -} diff --git a/config/postprocessDist.ts b/config/postprocessDist.ts index cddb75ff7e2..f8798c654d1 100644 --- a/config/postprocessDist.ts +++ b/config/postprocessDist.ts @@ -3,9 +3,6 @@ import * as path from "path"; import resolve from "resolve"; import { distDir, eachFile, reparse, reprint } from './helpers'; -import { applyDistSpotFixes } from "./distSpotFixes"; -applyDistSpotFixes(); - // The primary goal of the 'npm run resolve' script is to make ECMAScript // modules exposed by Apollo Client easier to consume natively in web browsers, // without bundling, and without help from package.json files. It accomplishes diff --git a/config/processInvariants.ts b/config/processInvariants.ts index 6227acd5840..c63881befc9 100644 --- a/config/processInvariants.ts +++ b/config/processInvariants.ts @@ -1,5 +1,5 @@ import * as fs from "fs"; -import * as path from "path"; +import { posix, join as osPathJoin } from "path"; import { distDir, eachFile, reparse, reprint } from './helpers'; eachFile(distDir, (file, relPath) => { @@ -10,7 +10,7 @@ eachFile(distDir, (file, relPath) => { } }).then(() => { fs.writeFileSync( - path.join(distDir, "invariantErrorCodes.js"), + osPathJoin(distDir, "invariantErrorCodes.js"), recast.print(errorCodeManifest, { tabWidth: 2, }).code + "\n", @@ -56,7 +56,7 @@ function getErrorCode( return numLit; } -function transform(code: string, file: string) { +function transform(code: string, relativeFilePath: string) { // If the code doesn't seem to contain anything invariant-related, we // can skip parsing and transforming it. if (!/invariant/i.test(code)) { @@ -64,6 +64,7 @@ function transform(code: string, file: string) { } const ast = reparse(code); + let addedDEV = false; recast.visit(ast, { visitCallExpression(path) { @@ -76,8 +77,9 @@ function transform(code: string, file: string) { } const newArgs = node.arguments.slice(0, 1); - newArgs.push(getErrorCode(file, node)); + newArgs.push(getErrorCode(relativeFilePath, node)); + addedDEV = true; return b.conditionalExpression( makeDEVExpr(), node, @@ -94,6 +96,7 @@ function transform(code: string, file: string) { if (isDEVLogicalAnd(path.parent.node)) { return; } + addedDEV = true; return b.logicalExpression("&&", makeDEVExpr(), node); } }, @@ -106,8 +109,9 @@ function transform(code: string, file: string) { return; } - const newArgs = [getErrorCode(file, node)]; + const newArgs = [getErrorCode(relativeFilePath, node)]; + addedDEV = true; return b.conditionalExpression( makeDEVExpr(), node, @@ -120,11 +124,58 @@ function transform(code: string, file: string) { } }); + if (addedDEV) { + // Make sure there's an import { __DEV__ } from "../utilities/globals" or + // similar declaration in any module where we injected __DEV__. + let foundExistingImportDecl = false; + + recast.visit(ast, { + visitImportDeclaration(path) { + this.traverse(path); + const node = path.node; + const importedModuleId = node.source.value; + + // Normalize node.source.value relative to the current file. + if ( + typeof importedModuleId === "string" && + importedModuleId.startsWith(".") + ) { + 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__"))) { + return false; + } + if (!node.specifiers) node.specifiers = []; + node.specifiers.push(b.importSpecifier(b.identifier("__DEV__"))); + return false; + } + } + } + }); + + if (!foundExistingImportDecl) { + // We could modify the AST to include a new import declaration, but since + // 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( + posix.dirname(relativeFilePath), + "utilities/globals", + ) + } in ${relativeFilePath}`); + } + } + return reprint(ast); } -function isIdWithName(node: Node, ...names: string[]) { - return n.Identifier.check(node) && +function isIdWithName(node: Node | null | undefined, ...names: string[]) { + return node && n.Identifier.check(node) && names.some(name => name === node.name); } diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index b7f925d5b9d..275ce428b99 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -391,12 +391,21 @@ Array [ "hasAnyDirectives", "hasClientExports", "hasDirectives", + "isAsyncIterableIterator", + "isBlob", "isDocumentNode", + "isExecutionPatchIncrementalResult", + "isExecutionPatchInitialResult", + "isExecutionPatchResult", "isField", "isInlineFragment", + "isNodeReadableStream", + "isNodeResponse", "isNonEmptyArray", "isNonNullObject", + "isReadableStream", "isReference", + "isStreamableBlob", "iterateObserversSafely", "makeReference", "makeUniqueId", @@ -404,6 +413,7 @@ Array [ "maybeDeepFreeze", "mergeDeep", "mergeDeepArray", + "mergeIncrementalData", "mergeOptions", "offsetLimitPagination", "relayStylePagination", @@ -424,7 +434,7 @@ exports[`exports of public entry points @apollo/client/utilities/globals 1`] = ` Array [ "DEV", "InvariantError", - "checkDEV", + "__DEV__", "global", "invariant", "maybe", diff --git a/src/cache/inmemory/__tests__/roundtrip.ts b/src/cache/inmemory/__tests__/roundtrip.ts index 7e1bd70a762..4ff053812ee 100644 --- a/src/cache/inmemory/__tests__/roundtrip.ts +++ b/src/cache/inmemory/__tests__/roundtrip.ts @@ -1,3 +1,4 @@ +import { __DEV__ } from "../../../utilities/globals"; import { DocumentNode } from 'graphql'; import gql from 'graphql-tag'; diff --git a/src/cache/inmemory/object-canon.ts b/src/cache/inmemory/object-canon.ts index 7f565d192ac..0e05611dd4c 100644 --- a/src/cache/inmemory/object-canon.ts +++ b/src/cache/inmemory/object-canon.ts @@ -1,5 +1,4 @@ -import "../../utilities/globals"; - +import { __DEV__ } from "../../utilities/globals"; import { Trie } from "@wry/trie"; import { canUseWeakMap, diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index 19c574c0af9..5f7b76d3285 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError } from '../../utilities/globals'; +import { invariant, InvariantError, __DEV__ } from '../../utilities/globals'; import { InlineFragmentNode, diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index 20e5a6bfe4d..2ae56785e40 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError } from '../../utilities/globals'; +import { invariant, InvariantError, __DEV__ } from '../../utilities/globals'; import { DocumentNode, diff --git a/src/cache/inmemory/writeToStore.ts b/src/cache/inmemory/writeToStore.ts index 6d511a79ef1..a778749b253 100644 --- a/src/cache/inmemory/writeToStore.ts +++ b/src/cache/inmemory/writeToStore.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError } from '../../utilities/globals'; +import { invariant, InvariantError, __DEV__ } from '../../utilities/globals'; import { equal } from '@wry/equality'; import { Trie } from '@wry/trie'; import { diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index fb2c328ab03..f21457a8f47 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError } from '../utilities/globals'; +import { invariant, InvariantError, __DEV__ } from '../utilities/globals'; import { ExecutionResult, DocumentNode } from 'graphql'; diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index c510ca658cd..a72b5669f62 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -1,4 +1,4 @@ -import { invariant } from '../utilities/globals'; +import { invariant, __DEV__ } from '../utilities/globals'; import { DocumentNode } from 'graphql'; import { equal } from '@wry/equality'; diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index 30edbc55cf1..0288f2d598c 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -3,7 +3,7 @@ import { equal } from "@wry/equality"; import { Cache, ApolloCache } from '../cache'; import { DeepMerger } from "../utilities" -import { mergeIncrementalData } from '../utilities/common/incrementalResult'; +import { mergeIncrementalData } from '../utilities'; import { WatchQueryOptions, ErrorPolicy } from './watchQueryOptions'; import { ObservableQuery, reobserveCacheFirst } from './ObservableQuery'; import { QueryListener } from './types'; diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 66b31daf630..b7d80957268 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -1,4 +1,4 @@ -import { invariant, InvariantError } from '../utilities/globals'; +import { invariant, InvariantError, __DEV__ } from '../utilities/globals'; import { DocumentNode } from 'graphql'; // TODO(brian): A hack until this issue is resolved (https://github.com/graphql/graphql-js/issues/3356) @@ -9,7 +9,7 @@ import { ApolloLink, execute, FetchResult } from '../link/core'; import { isExecutionPatchIncrementalResult, isExecutionPatchResult, -} from '../utilities/common/incrementalResult'; +} from '../utilities'; import { Cache, ApolloCache, canonicalStringify } from '../cache'; import { diff --git a/src/core/index.ts b/src/core/index.ts index 0af6d5dad1c..aed3eef66b0 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,6 +1,6 @@ /* Core */ -import { DEV } from '../utilities/globals'; +import { __DEV__ } from '../utilities/globals'; export { ApolloClient, @@ -90,7 +90,7 @@ export { // Note that all invariant.* logging is hidden in production. import { setVerbosity } from "ts-invariant"; export { setVerbosity as setLogVerbosity } -setVerbosity(DEV ? "log" : "silent"); +setVerbosity(__DEV__ ? "log" : "silent"); // Note that importing `gql` by itself, then destructuring // additional properties separately before exporting, is intentional. diff --git a/src/link/http/createHttpLink.ts b/src/link/http/createHttpLink.ts index 3f6a3fe605d..03c8e43d927 100644 --- a/src/link/http/createHttpLink.ts +++ b/src/link/http/createHttpLink.ts @@ -1,4 +1,4 @@ -import '../../utilities/globals'; +import { __DEV__ } from '../../utilities/globals'; import { visit, DefinitionNode, VariableDefinitionNode } from 'graphql'; diff --git a/src/link/http/responseIterator.ts b/src/link/http/responseIterator.ts index a0e565e464c..4698890a452 100644 --- a/src/link/http/responseIterator.ts +++ b/src/link/http/responseIterator.ts @@ -11,7 +11,7 @@ import { isNodeReadableStream, isReadableStream, isStreamableBlob, -} from "../../utilities/common/responseIterator"; +} from "../../utilities"; import asyncIterator from "./iterators/async"; import nodeStreamIterator from "./iterators/nodeStream"; diff --git a/src/react/hoc/__tests__/queries/recomposeWithState.ts b/src/react/hoc/__tests__/queries/recomposeWithState.ts index 1608f2fa7bf..92f97a0f8e7 100644 --- a/src/react/hoc/__tests__/queries/recomposeWithState.ts +++ b/src/react/hoc/__tests__/queries/recomposeWithState.ts @@ -2,7 +2,7 @@ // to avoid incurring an indirect dependency on ua-parser-js via fbjs. import React, { createFactory, Component } from "react"; -import '../../../../utilities/globals'; // For __DEV__ +import { __DEV__ } from "../../../../utilities/globals"; const setStatic = (key: string, value: string) => (BaseComponent: React.ComponentClass) => { diff --git a/src/react/hooks/internal/useIsomorphicLayoutEffect.ts b/src/react/hooks/internal/useIsomorphicLayoutEffect.ts index 7135b952e5c..203cfdcc42b 100644 --- a/src/react/hooks/internal/useIsomorphicLayoutEffect.ts +++ b/src/react/hooks/internal/useIsomorphicLayoutEffect.ts @@ -1,5 +1,5 @@ import { useLayoutEffect, useEffect } from 'react'; -import { canUseLayoutEffect } from '../../../utilities/common/canUse'; +import { canUseLayoutEffect } from '../../../utilities'; export const useIsomorphicLayoutEffect = canUseLayoutEffect ? useLayoutEffect diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts index bce2aa555b6..03905531ab5 100644 --- a/src/react/hooks/useSubscription.ts +++ b/src/react/hooks/useSubscription.ts @@ -1,8 +1,7 @@ -import '../../utilities/globals'; +import { invariant } from '../../utilities/globals'; import { useState, useRef, useEffect } from 'react'; import { DocumentNode } from 'graphql'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; -import { invariant } from '../../utilities/globals' import { equal } from '@wry/equality'; import { DocumentType, verifyDocumentType } from '../parser'; diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index 0871d1edd93..1bd886bc988 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -1,3 +1,4 @@ +import { invariant, __DEV__ } from '../../utilities/globals'; import { useRef, useEffect, useCallback, useMemo, useState } from 'react'; import { equal } from '@wry/equality'; import { @@ -11,7 +12,6 @@ import { WatchQueryOptions, WatchQueryFetchPolicy, } from '../../core'; -import { invariant } from '../../utilities/globals'; import { compact, Concast, diff --git a/src/react/hooks/useSyncExternalStore.ts b/src/react/hooks/useSyncExternalStore.ts index 55e5a71dddc..9cb6a624ad5 100644 --- a/src/react/hooks/useSyncExternalStore.ts +++ b/src/react/hooks/useSyncExternalStore.ts @@ -1,4 +1,4 @@ -import { invariant } from '../../utilities/globals'; +import { invariant, __DEV__ } from '../../utilities/globals'; import * as React from 'react'; import { canUseLayoutEffect } from '../../utilities'; diff --git a/src/utilities/common/maybeDeepFreeze.ts b/src/utilities/common/maybeDeepFreeze.ts index 947e6ee31f9..f25354309d1 100644 --- a/src/utilities/common/maybeDeepFreeze.ts +++ b/src/utilities/common/maybeDeepFreeze.ts @@ -1,4 +1,4 @@ -import '../globals'; // For __DEV__ +import { __DEV__ } from '../globals'; import { isNonNullObject } from './objects'; function deepFreeze(value: any) { diff --git a/src/utilities/globals/DEV.ts b/src/utilities/globals/DEV.ts index 540f64813cf..cfc1ad326b8 100644 --- a/src/utilities/globals/DEV.ts +++ b/src/utilities/globals/DEV.ts @@ -1,29 +1,20 @@ import global from "./global"; import { maybe } from "./maybe"; -// To keep string-based find/replace minifiers from messing with __DEV__ inside -// string literals or properties like global.__DEV__, we construct the "__DEV__" -// string in a roundabout way that won't be altered by find/replace strategies. -const __ = "__"; -const GLOBAL_KEY = [__, __].join("DEV"); +export default ( + "__DEV__" in global + // We want it to be possible to set __DEV__ globally to control the result + // of this code, so it's important to check global.__DEV__ instead of + // assuming a naked reference like __DEV__ refers to global scope, since + // those references could be replaced with true or false by minifiers. + ? Boolean(global.__DEV__) -function getDEV() { - try { - return Boolean(__DEV__); - } catch { - Object.defineProperty(global, GLOBAL_KEY, { - // In a buildless browser environment, maybe(() => process.env.NODE_ENV) - // evaluates as undefined, so __DEV__ becomes true by default, but can be - // initialized to false instead by a script/module that runs earlier. - value: maybe(() => process.env.NODE_ENV) !== "production", - enumerable: false, - configurable: true, - writable: true, - }); - // Using computed property access rather than global.__DEV__ here prevents - // string-based find/replace strategies from munging this to global.false: - return (global as any)[GLOBAL_KEY]; - } -} - -export default getDEV(); + // In a buildless browser environment, maybe(() => process.env.NODE_ENV) + // evaluates to undefined, so __DEV__ becomes true by default, but can be + // initialized to false instead by a script/module that runs earlier. + // + // If you use tooling to replace process.env.NODE_ENV with a string like + // "development", this code will become something like maybe(() => + // "development") !== "production", which also works as expected. + : maybe(() => process.env.NODE_ENV) !== "production" +); diff --git a/src/utilities/globals/global.ts b/src/utilities/globals/global.ts index 375689eb62c..0efe7280231 100644 --- a/src/utilities/globals/global.ts +++ b/src/utilities/globals/global.ts @@ -1,25 +1,5 @@ import { maybe } from "./maybe"; -declare global { - // Despite our attempts to reuse the React Native __DEV__ constant instead of - // inventing something new and Apollo-specific, declaring a useful type for - // __DEV__ unfortunately conflicts (TS2451) with the global declaration in - // @types/react-native/index.d.ts. - // - // To hide that harmless conflict, we @ts-ignore this line, which should - // continue to provide a type for __DEV__ elsewhere in the Apollo Client - // codebase, even when @types/react-native is not in use. - // - // However, because TypeScript drops @ts-ignore comments when generating .d.ts - // files (https://github.com/microsoft/TypeScript/issues/38628), we also - // sanitize the dist/utilities/globals/global.d.ts file to avoid declaring - // __DEV__ globally altogether when @apollo/client is installed in the - // node_modules directory of an application. - // - // @ts-ignore - const __DEV__: boolean | undefined; -} - export default ( maybe(() => globalThis) || maybe(() => window) || @@ -33,5 +13,5 @@ export default ( // is an arms race you cannot win, at least not in JavaScript. maybe(function() { return maybe.constructor("return this")() }) ) as typeof globalThis & { - __DEV__: typeof __DEV__; + __DEV__?: boolean; }; diff --git a/src/utilities/globals/index.ts b/src/utilities/globals/index.ts index 484eeca079d..acfe07a6136 100644 --- a/src/utilities/globals/index.ts +++ b/src/utilities/globals/index.ts @@ -3,10 +3,8 @@ import { invariant, InvariantError } from "ts-invariant"; // Just in case the graphql package switches from process.env.NODE_ENV to // __DEV__, make sure __DEV__ is polyfilled before importing graphql. import DEV from "./DEV"; -export { DEV } -export function checkDEV() { - invariant("boolean" === typeof DEV, DEV); -} +export { DEV }; +export const __DEV__ = DEV; // Import graphql/jsutils/instanceOf safely, working around its unchecked usage // of process.env.NODE_ENV and https://github.com/graphql/graphql-js/pull/2894. @@ -19,7 +17,3 @@ removeTemporaryGlobals(); export { maybe } from "./maybe"; export { default as global } from "./global"; export { invariant, InvariantError } - -// Ensure __DEV__ was properly initialized, and prevent tree-shaking bundlers -// from mistakenly pruning the ./DEV module (see issue #8674). -checkDEV(); diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 4f6f4697897..a5ff2844501 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -96,5 +96,7 @@ export * from './common/compact'; export * from './common/makeUniqueId'; export * from './common/stringifyForDisplay'; export * from './common/mergeOptions'; +export * from './common/responseIterator'; +export * from './common/incrementalResult'; export * from './types/IsStrictlyAny';