Repository: FormidableLabs/urql
Pull request #426 (2019-09-16 02:43:36)

Replace fast-json-stable-stringify
The problem with having `fast-json-stable-stringify` is that it's the only package that doesn't support ESM. This means that by using it we completely forgo the possibility of supporting ESM browser imports even for `urql/core` in the future.

This replaces the package with an embedded implementation that works the exact same. It's exported so that it can be reused in `@urql/exchange-graphcache`, however it works the same, so it's completely backwards compatible.

This also shaves off some bytes, since we're getting rid of some options and configuration in `fast-json-stable-stringify` that we don't need.


Patch:
diff --git a/package.json b/package.json
index 6b871837..fd3876d0 100644
--- a/package.json
+++ b/package.json
@@ -86,7 +86,6 @@
     "@testing-library/react": "^9.1.3",
     "@testing-library/react-hooks": "^2.0.1",
     "@types/enzyme": "3.10.3",
-    "@types/fast-json-stable-stringify": "^2.0.0",
     "@types/jest": "^24.0.18",
     "@types/react": "^16.9.2",
     "@types/react-test-renderer": "^16.9.0",
@@ -134,7 +133,6 @@
     "react-dom": ">= 16.8.0"
   },
   "dependencies": {
-    "fast-json-stable-stringify": "^2.0.0",
     "wonka": "^3.2.1"
   }
 }
diff --git a/src/index.ts b/src/index.ts
index fb48863b..d827ce4b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,6 +7,7 @@ export * from './types';
 
 export {
   CombinedError,
+  stringifyVariables,
   createRequest,
   makeResult,
   makeErrorResult,
diff --git a/src/utils/index.ts b/src/utils/index.ts
index e17b2d56..8c01fe95 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -3,6 +3,7 @@ export * from './request';
 export * from './result';
 export * from './typenames';
 export * from './toSuspenseSource';
+export * from './stringifyVariables';
 export * from './withPromise';
 
 export const noop = () => {
diff --git a/src/utils/request.ts b/src/utils/request.ts
index f5ab63c3..5d33de19 100644
--- a/src/utils/request.ts
+++ b/src/utils/request.ts
@@ -1,6 +1,6 @@
 import { DocumentNode, parse, print } from 'graphql';
 import { hash, phash } from './hash';
-import stringify from 'fast-json-stable-stringify';
+import { stringifyVariables } from './stringifyVariables';
 import { GraphQLRequest, Operation, OperationContext } from '../types';
 
 interface Documents {
@@ -33,7 +33,7 @@ export const createRequest = (
   (query as any)[keyProp] = key;
 
   return {
-    key: vars ? phash(key, stringify(vars)) >>> 0 : key,
+    key: vars ? phash(key, stringifyVariables(vars)) >>> 0 : key,
     query,
     variables: vars || {},
   };
diff --git a/src/utils/stringifyVariables.ts b/src/utils/stringifyVariables.ts
new file mode 100644
index 00000000..38c060b2
--- /dev/null
+++ b/src/utils/stringifyVariables.ts
@@ -0,0 +1,50 @@
+const seen = new Set();
+
+const stringify = (x: any): string => {
+  if (x === undefined) {
+    return '';
+  } else if (typeof x == 'number') {
+    return isFinite(x) ? '' + x : 'null';
+  } else if (typeof x !== 'object') {
+    return JSON.stringify(x);
+  } else if (x === null) {
+    return 'null';
+  }
+
+  let out = '';
+  if (Array.isArray(x)) {
+    out = '[';
+    for (let i = 0, l = x.length; i < l; i++) {
+      if (i > 0) out += ',';
+      const value = stringify(x[i]);
+      out += value.length > 0 ? value : 'null';
+    }
+
+    out += ']';
+    return out;
+  } else if (seen.has(x)) {
+    throw new TypeError('Converting circular structure to JSON');
+  }
+
+  const keys = Object.keys(x).sort();
+
+  seen.add(x);
+  out = '{';
+  for (let i = 0, l = keys.length; i < l; i++) {
+    const key = keys[i];
+    const value = stringify(x[key]);
+    if (value.length !== 0) {
+      if (out.length > 1) out += ',';
+      out += stringify(key) + ':' + value;
+    }
+  }
+
+  seen.delete(x);
+  out += '}';
+  return out;
+};
+
+export const stringifyVariables = (x: any): string => {
+  seen.clear();
+  return stringify(x);
+};
diff --git a/yarn.lock b/yarn.lock
index 807a433b..898e5ad2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -456,11 +456,6 @@
   resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
   integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
 
-"@types/fast-json-stable-stringify@^2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@types/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#40363bb847cb86b2c2e1599f1398d11e8329c921"
-  integrity sha512-mky/O83TXmGY39P1H9YbUpjV6l6voRYlufqfFCvel8l1phuy8HRjdWc1rrPuN53ITBJlbyMSV6z3niOySO5pgQ==
-
 "@types/glob@^7.1.1":
   version "7.1.1"
   resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"


Test patch:
diff --git a/src/utils/stringifyVariables.test.ts b/src/utils/stringifyVariables.test.ts
new file mode 100644
index 00000000..e08c21b6
--- /dev/null
+++ b/src/utils/stringifyVariables.test.ts
@@ -0,0 +1,30 @@
+import { stringifyVariables } from './stringifyVariables';
+
+it('stringifies objects stabily', () => {
+  expect(stringifyVariables({ b: 'b', a: 'a' })).toBe('{"a":"a","b":"b"}');
+  expect(stringifyVariables({ x: { b: 'b', a: 'a' } })).toBe(
+    '{"x":{"a":"a","b":"b"}}'
+  );
+});
+
+it('stringifies arrays', () => {
+  expect(stringifyVariables([1, 2])).toBe('[1,2]');
+  expect(stringifyVariables({ x: [1, 2] })).toBe('{"x":[1,2]}');
+});
+
+it('stringifies scalars', () => {
+  expect(stringifyVariables(1)).toBe('1');
+  expect(stringifyVariables('test')).toBe('"test"');
+  expect(stringifyVariables(null)).toBe('null');
+  expect(stringifyVariables(undefined)).toBe('');
+  expect(stringifyVariables(Infinity)).toBe('null');
+  expect(stringifyVariables(1 / 0)).toBe('null');
+});
+
+it('throws for circular structures', () => {
+  expect(() => {
+    const x = { x: null } as any;
+    x.x = x;
+    stringifyVariables(x);
+  }).toThrow();
+});