diff --git a/package-lock.json b/package-lock.json index bfbbefad08..e976a74a26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -831,6 +831,57 @@ "to-fast-properties": "^2.0.0" } }, + "@types/chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-zw8UvoBEImn392tLjxoavuonblX/4Yb9ha4KBU10FirCfwgzhKO0dvyJSF9ByxV1xK1r2AgnAi/tvQaLgxQqxA==", + "dev": true + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, + "@types/node": { + "version": "12.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.2.tgz", + "integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==", + "dev": true + }, + "@types/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-bZgjwIWu9gHCjirKJoOlLzGi5N0QgZ5t7EXEuoqyWCHTuSddURXo3FOBYDyRPNOWzZ6NbkLvZnVkn483Y/tvcQ==", + "dev": true, + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, "acorn": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", @@ -1814,6 +1865,12 @@ } } }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -2741,6 +2798,15 @@ "read-pkg": "^3.0.0" } }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -2932,6 +2998,17 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shelljs": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", + "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", diff --git a/package.json b/package.json index bce6fa92ad..1f95ab94e8 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,11 @@ "node": ">= 6.x" }, "scripts": { - "test": "npm run prettier:check && npm run lint && npm run check && npm run testonly", + "test": "npm run prettier:check && npm run lint && npm run check && npm run testonly && npm run test:double-dependency", "test:ci": "npm run prettier:check && npm run lint -- --no-cache && npm run check && npm run testonly:cover && npm run build", - "testonly": "mocha -r ts-node/register --full-trace src/**/__tests__/**/*-test.js", + "testonly": "mocha -r ts-node/register --full-trace src/**/__tests__/**/*-test.js src/**/__tests__/**/*-test.ts", "testonly:cover": "nyc npm run testonly", + "test:double-dependency": "npm run build && mocha -r ts-node/register --full-trace src/__tests__/double-dependency.test.ts; rm -rf ./dist1", "lint": "eslint --cache --report-unused-disable-directives src resources", "benchmark": "node --noconcurrent_sweeping --expose-gc --predictable ./resources/benchmark.js", "prettier": "prettier --ignore-path .gitignore --write --list-different \"**/*.{js,md,json,yml}\"", @@ -54,6 +55,10 @@ "@babel/polyfill": "7.4.4", "@babel/preset-env": "7.5.5", "@babel/register": "7.5.5", + "@types/chai": "^4.2.0", + "@types/mocha": "^5.2.7", + "@types/node": "^12.7.2", + "@types/shelljs": "^0.8.5", "babel-eslint": "10.0.2", "chai": "4.2.0", "eslint": "5.16.0", @@ -62,6 +67,7 @@ "mocha": "6.2.0", "nyc": "14.1.1", "prettier": "1.18.2", + "shelljs": "0.8.3", "ts-node": "^8.3.0", "typescript": "^3.5.3" } diff --git a/src/__tests__/double-dependency.test.ts b/src/__tests__/double-dependency.test.ts new file mode 100644 index 0000000000..95b663c42e --- /dev/null +++ b/src/__tests__/double-dependency.test.ts @@ -0,0 +1,35 @@ +import * as shelljs from 'shelljs'; +import { join } from 'path'; + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +describe('graphql-js module', () => { + it('allows interop between two versions of iteslf', () => { + const warn = console.warn; + const warnedWith = []; + console.warn = message => warnedWith.push(message); + const root = shelljs.pwd().stdout; + + const g1Path = 'dist'; + const g2Path = 'dist1'; + + shelljs.cp('-r', g1Path, g2Path); + + const g1 = require(join(root, g1Path)); + const g2 = require(join(root, g2Path)); + + const obj = new g1.GraphQLObjectType({ + name: 'Dog', + fields: { id: { type: g1.GraphQLString } }, + }); + + expect(g1.isObjectType(obj)).to.be.true; + expect(g2.isObjectType(obj)).to.be.true; + expect(warnedWith[0]).to.include( + 'Using GraphQLObjectType "Dog" from another module or realm.', + ); + expect(warnedWith[1]).to.be.undefined; + console.warn = warn; + }).timeout(5000); +}); diff --git a/src/jsutils/__tests__/instanceOf-test.js b/src/jsutils/__tests__/instanceOf-test.js deleted file mode 100644 index b91f03f9ce..0000000000 --- a/src/jsutils/__tests__/instanceOf-test.js +++ /dev/null @@ -1,24 +0,0 @@ -// @flow strict - -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import instanceOf from '../instanceOf'; - -describe('instanceOf', () => { - it('fails with descriptive error message', () => { - function getFoo() { - class Foo {} - return Foo; - } - const Foo1 = getFoo(); - const Foo2 = getFoo(); - - expect(() => instanceOf(new Foo1(), Foo2)).to.throw( - /^Cannot use Foo "\[object Object\]" from another module or realm./m, - ); - expect(() => instanceOf(new Foo2(), Foo1)).to.throw( - /^Cannot use Foo "\[object Object\]" from another module or realm./m, - ); - }); -}); diff --git a/src/jsutils/__tests__/instanceOf-test.ts b/src/jsutils/__tests__/instanceOf-test.ts new file mode 100644 index 0000000000..0cb24e2581 --- /dev/null +++ b/src/jsutils/__tests__/instanceOf-test.ts @@ -0,0 +1,38 @@ +// @flow strict + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import instanceOf from '../instanceOf'; + +describe('instanceOf', () => { + it('fails with descriptive error message', () => { + let calledWith = []; + const warn = console.warn; + console.warn = message => calledWith.push(message); + + function getFoo() { + class Foo {} + return Foo; + } + + function getBar() { + return Bar; + } + + const Foo1 = getFoo(); + const Foo2 = getFoo(); + + class Bar {} + const bar = new Bar(); + + expect(calledWith[0]).to.be.undefined; + expect(instanceOf(new Foo1(), Foo2)).to.be.true; + expect(calledWith[0]).to.not.be.undefined; + expect(instanceOf(new Foo2(), Foo1)).to.be.true; + expect(calledWith[1]).to.be.undefined; // only warn once + + expect(instanceOf(bar, Foo1)).to.be.false; + console.warn = warn; + }); +}); diff --git a/src/jsutils/instanceOf.js b/src/jsutils/instanceOf.js deleted file mode 100644 index 967969ab21..0000000000 --- a/src/jsutils/instanceOf.js +++ /dev/null @@ -1,45 +0,0 @@ -// @flow strict - -/** - * A replacement for instanceof which includes an error warning when multi-realm - * constructors are detected. - */ -declare function instanceOf( - value: mixed, - constructor: mixed, -): boolean %checks(value instanceof constructor); - -// See: https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production -// See: https://webpack.js.org/guides/production/ -export default process.env.NODE_ENV === 'production' - ? // eslint-disable-next-line no-shadow - function instanceOf(value: mixed, constructor: mixed) { - return value instanceof constructor; - } - : // eslint-disable-next-line no-shadow - function instanceOf(value: any, constructor: any) { - if (value instanceof constructor) { - return true; - } - if (value) { - const valueClass = value.constructor; - const className = constructor.name; - if (className && valueClass && valueClass.name === className) { - throw new Error( - `Cannot use ${className} "${value}" from another module or realm. - -Ensure that there is only one instance of "graphql" in the node_modules -directory. If different versions of "graphql" are the dependencies of other -relied on modules, use "resolutions" to ensure only one version is installed. - -https://yarnpkg.com/en/docs/selective-version-resolutions - -Duplicate "graphql" modules cannot be used at the same time since different -versions may have different capabilities and behavior. The data from one -version used in the function from another could produce confusing and -spurious results.`, - ); - } - } - return false; - }; diff --git a/src/jsutils/instanceOf.js.flow b/src/jsutils/instanceOf.js.flow new file mode 100644 index 0000000000..14ffb7321b --- /dev/null +++ b/src/jsutils/instanceOf.js.flow @@ -0,0 +1,10 @@ +// @flow strict + +/** + * A replacement for instanceof which includes an error warning when multi-realm + * constructors are detected. + */ +declare export default function instanceOf( + value: mixed, + constructor: mixed, +): boolean %checks(value instanceof constructor); diff --git a/src/jsutils/instanceOf.ts b/src/jsutils/instanceOf.ts new file mode 100644 index 0000000000..890f601d87 --- /dev/null +++ b/src/jsutils/instanceOf.ts @@ -0,0 +1,48 @@ +/** + * A replacement for instanceof which includes an error warning when multi-realm + * constructors are detected. + */ + +// Keep the message around, but only show it once (per instance of graphql-js that encounters mismatched constructors) +let warned = false; +// See: https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production +// See: https://webpack.js.org/guides/production/ +export default process.env.NODE_ENV === 'production' + ? // eslint-disable-next-line no-shadow + function instanceOf( + value: unknown, + constructor: new () => T, + ): value is T { + return ( + value instanceof constructor || + (value != null && + value.constructor != null && + constructor.name != null && + value.constructor.name == constructor.name) + ); + } + : // eslint-disable-next-line no-shadow + function instanceOf( + value: unknown, + constructor: new () => T, + ): value is T { + if (value instanceof constructor) { + return true; + } + if (value) { + const valueClass = value.constructor; + const className = constructor.name; + if (className && valueClass && valueClass.name === className) { + if (!warned) { + console.warn( + `@apollo/graphql: Using ${className} "${value}" from another module or realm. +This could produce confusing and spurious results, especially if your "graphql"/"@apollo/graphql" versions are inconsistent. +You can use \`npm ls graphql\` and \`npm ls @apollo/graphql\` to ensure all versions are consistent.`, // TODO: determine what we mean by "consistent" + ); + warned = true; + } + return true; + } + } + return false; + };