Skip to content

Commit eb1532c

Browse files
committed
Experiment: use __DEV__ instead of process.env.NODE_ENV.
As explained in #8266, our use of process.env.NODE_ENV requires those expressions to be either replaced with a string literal by a minifier (common convention in the React ecosystem) or polyfilled globally. We stopped polyfilling process.env globally in the ts-invariant package in apollographql/invariant-packages#94, but @apollo/client is still relying on process.env internally, as is the graphql package. If we want to rid ourselves fully of the drawbacks of process.env.NODE_ENV, we probably ought to stop using it ourselves. Though most developers in the React ecosystem will be using a bundler or minifier that replaces process.env.NODE_ENV at build time, you may not have the luxury of custom minification when loading @apollo/client from a CDN, which leaves only the option of a global process polyfill, which is problematic because that's how some applications detect if the current code is running Node.js (more details/links in #8266). Instead, I believe we can (and must?) stop using process.env.NODE_ENV, and one of many better alternatives appears to be the __DEV__ variable used by React Native, which is much easier to polyfill, since it's a single variable rather than a nested object. Switching to __DEV__ will initially cause a large bundle size regression (+3.5Kb *after* minification and gzip), but that's not technically a breaking change (and thus acceptable for AC3.4), and it should be easy to reconfigure minifiers to replace __DEV__ with true or false (or even just undefined), with sufficient guidance from the release notes. That still leaves the process.env.NODE_ENV check in instanceOf.mjs in the graphql package. Discussion in graphql/graphql-js#2894 suggests the plan is to stop using NODE_ENV altogether, which would be ideal, but that won't happen until graphql@16 at the earliest. To work around the problem in the meantime, I devised a strategy where we polyfill process.env.NODE_ENV only briefly, while we import code that depends on graphql/jsutils/instanceOf.mjs, and then synchronously remove the global polyfill, so it does not permanently pollute the global namespace. This strategy assumes @apollo/client is the first to import the graphql package. If you have other code that imports instanceOf.mjs earlier, and you don't have a process.env.NODE_ENV strategy already, it's your responsibility to make that import work, however you see fit. Apollo Client is only responsible for making sure its own imports of the graphql package have a chance of succeeding, a responsibility I believe my strategy fulfills cleverly if not elegantly. Of course, this charade does not happen if process.env.NODE_ENV is already defined or has been minified away, but only if accessing it would throw, since that's what we're trying to avoid. Although we could do some more work to reduce the bundle size impact of blindly switching to __DEV__, I believe this PR already solves the last remaining hurdles documented in #8266, potentially allowing @apollo/client/core@beta to be loaded from an ESM-aware CDN like esm.run or jspm.io. The default __DEV__ value will be true in those environments, but could be initialized differently by a script/module that runs earlier in the HTML of the page.
1 parent 14ffe47 commit eb1532c

23 files changed

+124
-141
lines changed

config/processInvariants.ts

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -71,50 +71,50 @@ function transform(code: string, file: string) {
7171
const node = path.node;
7272

7373
if (isCallWithLength(node, "invariant", 1)) {
74-
if (isNodeEnvConditional(path.parent.node)) {
74+
if (isDEVConditional(path.parent.node)) {
7575
return;
7676
}
7777

7878
const newArgs = node.arguments.slice(0, 1);
7979
newArgs.push(getErrorCode(file, node));
8080

8181
return b.conditionalExpression(
82-
makeNodeEnvTest(),
82+
makeDEVExpr(),
83+
node,
8384
b.callExpression.from({
8485
...node,
8586
arguments: newArgs,
8687
}),
87-
node,
8888
);
8989
}
9090

9191
if (node.callee.type === "MemberExpression" &&
9292
isIdWithName(node.callee.object, "invariant") &&
9393
isIdWithName(node.callee.property, "log", "warn", "error")) {
94-
if (isNodeEnvLogicalOr(path.parent.node)) {
94+
if (isDEVLogicalAnd(path.parent.node)) {
9595
return;
9696
}
97-
return b.logicalExpression("||", makeNodeEnvTest(), node);
97+
return b.logicalExpression("&&", makeDEVExpr(), node);
9898
}
9999
},
100100

101101
visitNewExpression(path) {
102102
this.traverse(path);
103103
const node = path.node;
104104
if (isCallWithLength(node, "InvariantError", 0)) {
105-
if (isNodeEnvConditional(path.parent.node)) {
105+
if (isDEVConditional(path.parent.node)) {
106106
return;
107107
}
108108

109109
const newArgs = [getErrorCode(file, node)];
110110

111111
return b.conditionalExpression(
112-
makeNodeEnvTest(),
112+
makeDEVExpr(),
113+
node,
113114
b.newExpression.from({
114115
...node,
115116
arguments: newArgs,
116117
}),
117-
node,
118118
);
119119
}
120120
}
@@ -137,32 +137,21 @@ function isCallWithLength(
137137
node.arguments.length > length;
138138
}
139139

140-
function isNodeEnvConditional(node: Node) {
140+
function isDEVConditional(node: Node) {
141141
return n.ConditionalExpression.check(node) &&
142-
isNodeEnvExpr(node.test);
142+
isDEVExpr(node.test);
143143
}
144144

145-
function isNodeEnvLogicalOr(node: Node) {
145+
function isDEVLogicalAnd(node: Node) {
146146
return n.LogicalExpression.check(node) &&
147-
node.operator === "||" &&
148-
isNodeEnvExpr(node.left);
147+
node.operator === "&&" &&
148+
isDEVExpr(node.left);
149149
}
150150

151-
function makeNodeEnvTest() {
152-
return b.binaryExpression(
153-
"===",
154-
b.memberExpression(
155-
b.memberExpression(
156-
b.identifier("process"),
157-
b.identifier("env")
158-
),
159-
b.identifier("NODE_ENV"),
160-
),
161-
b.stringLiteral("production"),
162-
);
151+
function makeDEVExpr() {
152+
return b.identifier("__DEV__");
163153
}
164154

165-
const referenceNodeEnvExpr = makeNodeEnvTest();
166-
function isNodeEnvExpr(node: Node) {
167-
return recast.types.astNodesAreEquivalent(node, referenceNodeEnvExpr);
155+
function isDEVExpr(node: Node) {
156+
return isIdWithName(node, "__DEV__");
168157
}

config/rollup.config.js

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function prepareCJSMinified(input) {
7171
compress: {
7272
toplevel: true,
7373
global_defs: {
74-
'@process.env.NODE_ENV': JSON.stringify('production'),
74+
'@__DEV__': 'false',
7575
},
7676
},
7777
}),
@@ -97,27 +97,6 @@ function prepareBundle({
9797
exports: 'named',
9898
interop: 'esModule',
9999
externalLiveBindings: false,
100-
// In Node.js, where these CommonJS bundles are most commonly used,
101-
// the expression process.env.NODE_ENV can be very expensive to
102-
// evaluate, because process.env is a wrapper for the actual OS
103-
// environment, and lookups are not cached. We need to preserve the
104-
// syntax of process.env.NODE_ENV expressions for dead code
105-
// elimination to work properly, but we can apply our own caching by
106-
// shadowing the global process variable with a stripped-down object
107-
// that saves a snapshot of process.env.NODE_ENV when the bundle is
108-
// first evaluated. If we ever need other process properties, we can
109-
// add more stubs here.
110-
intro: '!(function (process) {',
111-
outro: [
112-
'}).call(this, {',
113-
' env: {',
114-
' NODE_ENV: typeof process === "object"',
115-
' && process.env',
116-
' && process.env.NODE_ENV',
117-
' || "development"',
118-
' }',
119-
'});',
120-
].join('\n'),
121100
},
122101
plugins: [
123102
extensions ? nodeResolve({ extensions }) : nodeResolve(),

src/__tests__/__snapshots__/exports.ts.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Array [
1616
"NetworkStatus",
1717
"Observable",
1818
"ObservableQuery",
19+
"__DEV__",
1920
"checkFetcher",
2021
"concat",
2122
"createHttpLink",
@@ -67,6 +68,7 @@ Array [
6768
"InMemoryCache",
6869
"MissingFieldError",
6970
"Policies",
71+
"__DEV__",
7072
"cacheSlot",
7173
"canonicalStringify",
7274
"defaultDataIdFromObject",
@@ -90,6 +92,7 @@ Array [
9092
"NetworkStatus",
9193
"Observable",
9294
"ObservableQuery",
95+
"__DEV__",
9396
"checkFetcher",
9497
"concat",
9598
"createHttpLink",
@@ -224,6 +227,7 @@ Array [
224227
"ApolloConsumer",
225228
"ApolloProvider",
226229
"DocumentType",
230+
"__DEV__",
227231
"getApolloContext",
228232
"operationName",
229233
"parser",
@@ -320,6 +324,7 @@ Array [
320324
"Concast",
321325
"DeepMerger",
322326
"Observable",
327+
"__DEV__",
323328
"addTypenameToDocument",
324329
"argumentsObjectFromField",
325330
"asyncMap",

src/cache/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export { __DEV__ } from "../utilities";
2+
13
export { Transaction, ApolloCache } from './core/cache';
24
export { Cache } from './core/types/Cache';
35
export { DataProxy } from './core/types/DataProxy';

src/cache/inmemory/__tests__/roundtrip.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function storeRoundtrip(query: DocumentNode, result: any, variables = {}) {
5757
const immutableResult = readQueryFromStore(reader, readOptions);
5858
expect(immutableResult).toEqual(reconstructedResult);
5959
expect(readQueryFromStore(reader, readOptions)).toBe(immutableResult);
60-
if (process.env.NODE_ENV !== 'production') {
60+
if (__DEV__) {
6161
try {
6262
// Note: this illegal assignment will only throw in strict mode, but that's
6363
// safe to assume because this test file is a module.

src/cache/inmemory/object-canon.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export class ObjectCanon {
118118
// Since canonical arrays may be shared widely between
119119
// unrelated consumers, it's important to regard them as
120120
// immutable, even if they are not frozen in production.
121-
if (process.env.NODE_ENV !== "production") {
121+
if (__DEV__) {
122122
Object.freeze(array);
123123
}
124124
}
@@ -154,7 +154,7 @@ export class ObjectCanon {
154154
// Since canonical objects may be shared widely between
155155
// unrelated consumers, it's important to regard them as
156156
// immutable, even if they are not frozen in production.
157-
if (process.env.NODE_ENV !== "production") {
157+
if (__DEV__) {
158158
Object.freeze(obj);
159159
}
160160
}

src/cache/inmemory/readFromStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ export class StoreReader {
477477
}), i);
478478
}
479479

480-
if (process.env.NODE_ENV !== 'production') {
480+
if (__DEV__) {
481481
assertSelectionSetForIdValue(context.store, field, item);
482482
}
483483

src/cache/inmemory/writeToStore.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ export class StoreWriter {
269269
incomingFields = this.applyMerges(mergeTree, entityRef, incomingFields, context);
270270
}
271271

272-
if (process.env.NODE_ENV !== "production" && !context.overwrite) {
272+
if (__DEV__ && !context.overwrite) {
273273
const hasSelectionSet = (storeFieldName: string) =>
274274
fieldsWithSelectionSets.has(fieldNameFromStoreName(storeFieldName));
275275
const fieldsWithSelectionSets = new Set<string>();
@@ -319,7 +319,7 @@ export class StoreWriter {
319319
// In development, we need to clone scalar values so that they can be
320320
// safely frozen with maybeDeepFreeze in readFromStore.ts. In production,
321321
// it's cheaper to store the scalar values directly in the cache.
322-
return process.env.NODE_ENV === 'production' ? value : cloneDeep(value);
322+
return __DEV__ ? cloneDeep(value) : value;
323323
}
324324

325325
if (Array.isArray(value)) {

src/core/ApolloClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export class ApolloClient<TCacheShape> implements DataProxy {
150150
// devtools, but disable them by default in production.
151151
typeof window === 'object' &&
152152
!(window as any).__APOLLO_CLIENT__ &&
153-
process.env.NODE_ENV !== 'production',
153+
__DEV__,
154154
queryDeduplication = true,
155155
defaultOptions,
156156
assumeImmutableResults = false,
@@ -204,7 +204,7 @@ export class ApolloClient<TCacheShape> implements DataProxy {
204204
/**
205205
* Suggest installing the devtools for developers who don't have them
206206
*/
207-
if (!hasSuggestedDevtools && process.env.NODE_ENV !== 'production') {
207+
if (!hasSuggestedDevtools && __DEV__) {
208208
hasSuggestedDevtools = true;
209209
if (
210210
typeof window !== 'undefined' &&

src/core/ObservableQuery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export class ObservableQuery<
306306
const { updateQuery } = fetchMoreOptions;
307307

308308
if (updateQuery) {
309-
if (process.env.NODE_ENV !== "production" &&
309+
if (__DEV__ &&
310310
!warnedAboutUpdateQuery) {
311311
invariant.warn(
312312
`The updateQuery callback for fetchMore is deprecated, and will be removed

0 commit comments

Comments
 (0)