Skip to content

Commit ef851e0

Browse files
committed
use dev/prod condition to trigger instanceOf check
1 parent 00a1db0 commit ef851e0

File tree

7 files changed

+155
-64
lines changed

7 files changed

+155
-64
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
]
1313
}
1414
},
15-
"sideEffects": false,
15+
"sideEffects": [
16+
"./dev/index.js"
17+
],
1618
"homepage": "https://github.com/graphql/graphql-js",
1719
"bugs": {
1820
"url": "https://github.com/graphql/graphql-js/issues"

resources/build-npm.ts

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ await buildPackage('./npmDist');
1919
showDirStats('./npmDist');
2020

2121
async function buildPackage(outDir: string): Promise<void> {
22+
const devDir = path.join(outDir, '__dev__');
23+
2224
fs.rmSync(outDir, { recursive: true, force: true });
2325
fs.mkdirSync(outDir);
26+
fs.mkdirSync(devDir);
2427

25-
fs.copyFileSync('./LICENSE', `./${outDir}/LICENSE`);
26-
fs.copyFileSync('./README.md', `./${outDir}/README.md`);
28+
fs.copyFileSync('./LICENSE', `${outDir}/LICENSE`);
29+
fs.copyFileSync('./README.md', `${outDir}/README.md`);
2730

2831
const packageJSON = readPackageJSON();
2932

@@ -85,17 +88,61 @@ async function buildPackage(outDir: string): Promise<void> {
8588
extension: '.js',
8689
});
8790

88-
for (const filepath of emittedTSFiles) {
89-
if (path.basename(filepath) === 'index.js') {
90-
const relativePath =
91-
'./' + crossPlatformRelativePath('./npmDist', filepath);
92-
packageJSON.exports[path.dirname(relativePath)] = relativePath;
91+
for (const prodFile of emittedTSFiles) {
92+
const { dir, base } = path.parse(prodFile);
93+
94+
const match = base.match(/^([^.]*)\.?(.*)$/);
95+
assert(match);
96+
const [, name, ext] = match;
97+
98+
if (ext === 'js.map') {
99+
continue;
100+
} else if (path.basename(dir) === 'dev') {
101+
packageJSON.exports['./dev'] = './dev/index.js';
102+
continue;
103+
}
104+
105+
const relativePathToProd = crossPlatformRelativePath(prodFile, outDir);
106+
const relativePathAndName = crossPlatformRelativePath(
107+
outDir,
108+
`${dir}/${name}`,
109+
);
110+
111+
const lines =
112+
ext === 'd.ts' ? [] : [`import '${relativePathToProd}/dev/index.js';`];
113+
lines.push(
114+
`export * from '${relativePathToProd}/${relativePathAndName}.js';`,
115+
);
116+
const body = lines.join('\n');
117+
118+
writeGeneratedFile(
119+
path.join(devDir, path.relative(outDir, prodFile)),
120+
body,
121+
);
122+
123+
if (base === 'index.js') {
124+
const dirname = path.dirname(relativePathAndName);
125+
packageJSON.exports[dirname === '.' ? dirname : `./${dirname}`] = {
126+
development: `./__dev__/${relativePathAndName}.js`,
127+
default: `./${relativePathAndName}.js`,
128+
};
93129
}
94130
}
95131

96132
// Temporary workaround to allow "internal" imports, no grantees provided
97-
packageJSON.exports['./*.js'] = './*.js';
98-
packageJSON.exports['./*'] = './*.js';
133+
packageJSON.exports['./*.js'] = {
134+
development: './__dev__/*.js',
135+
default: './*.js',
136+
};
137+
packageJSON.exports['./*'] = {
138+
development: './__dev__/*.js',
139+
default: './*.js',
140+
};
141+
142+
packageJSON.sideEffects = [
143+
...(packageJSON.sideEffects as Array<string>),
144+
'__dev__/*',
145+
];
99146

100147
const packageJsonPath = `./${outDir}/package.json`;
101148
const prettified = await prettify(
@@ -127,7 +174,11 @@ function emitTSFiles(options: {
127174
const tsHost = ts.createCompilerHost(tsOptions);
128175
tsHost.writeFile = (filepath, body) => writeGeneratedFile(filepath, body);
129176

130-
const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost);
177+
const tsProgram = ts.createProgram(
178+
['src/index.ts', 'src/dev/index.ts'],
179+
tsOptions,
180+
tsHost,
181+
);
131182
const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, {
132183
after: [changeExtensionInImportPaths({ extension }), inlineInvariant],
133184
});

resources/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,15 @@ interface PackageJSON {
234234
repository?: { url?: string };
235235
scripts?: { [name: string]: string };
236236
type?: string;
237-
exports: { [path: string]: string };
237+
sideEffects?: boolean | Array<string>;
238+
exports: {
239+
[path: string]:
240+
| string
241+
| {
242+
development: string;
243+
default: string;
244+
};
245+
};
238246
types?: string;
239247
typesVersions: { [ranges: string]: { [path: string]: Array<string> } };
240248
devDependencies?: { [name: string]: string };

src/dev/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## GraphQL Dev
2+
3+
The `graphql/dev` module is responsible for enabling development mode.
4+
5+
In development mode, GraphQL.js provides additional checks to help avoid developer errors.
6+
7+
```js
8+
import 'graphql/dev'; // ES6
9+
require('graphql/dev'); // CommonJS
10+
```

src/dev/index.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { inspect } from '../jsutils/inspect.js';
2+
import type { Constructor } from '../jsutils/instanceOf.js';
3+
4+
/**
5+
* An additional check to be included within instanceOf warning when
6+
* multi-realm constructors are detected.
7+
*
8+
* This additional check will be included:
9+
* 1. if 'graphql/development.js' is imported explicitly prior to all
10+
* other imports from this library, or
11+
* 2. if the "development" condition is set.
12+
*/
13+
export function developmentInstanceOfCheck(
14+
value: unknown,
15+
constructor: Constructor,
16+
): void {
17+
if (typeof value === 'object' && value !== null) {
18+
// Prefer Symbol.toStringTag since it is immune to minification.
19+
const className = constructor.prototype[Symbol.toStringTag];
20+
const valueClassName =
21+
// We still need to support constructor's name to detect conflicts with older versions of this library.
22+
Symbol.toStringTag in value
23+
? value[Symbol.toStringTag]
24+
: value.constructor?.name;
25+
if (className === valueClassName) {
26+
const stringifiedValue = inspect(value);
27+
throw new Error(
28+
`Cannot use ${className} "${stringifiedValue}" from another module or realm.
29+
30+
Ensure that there is only one instance of "graphql" in the node_modules
31+
directory. If different versions of "graphql" are the dependencies of other
32+
relied on modules, use "resolutions" to ensure only one version is installed.
33+
34+
https://yarnpkg.com/en/docs/selective-version-resolutions
35+
36+
Duplicate "graphql" modules cannot be used at the same time since different
37+
versions may have different capabilities and behavior. The data from one
38+
version used in the function from another could produce confusing and
39+
spurious results.`,
40+
);
41+
}
42+
}
43+
}
44+
45+
(globalThis as any)[Symbol.for('graphql.instanceOfCheck')] =
46+
developmentInstanceOfCheck;

src/jsutils/__tests__/instanceOf-test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { expect } from 'chai';
22
import { describe, it } from 'mocha';
33

4-
import { instanceOf } from '../instanceOf.js';
4+
import { developmentInstanceOfCheck } from '../../dev/index.js';
5+
6+
(globalThis as any)[Symbol.for('graphql.instanceOfCheck')] =
7+
developmentInstanceOfCheck;
8+
9+
const { instanceOf } = await import(`../instanceOf.js?ts${Date.now()}`);
510

611
describe('instanceOf', () => {
712
it('do not throw on values without prototype', () => {

src/jsutils/instanceOf.ts

Lines changed: 20 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,27 @@
1-
import { inspect } from './inspect.js';
2-
3-
/* c8 ignore next 3 */
4-
const isProduction =
5-
globalThis.process != null &&
6-
// eslint-disable-next-line no-undef
7-
process.env.NODE_ENV === 'production';
8-
91
/**
10-
* A replacement for instanceof which includes an error warning when multi-realm
11-
* constructors are detected.
12-
* See: https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production
13-
* See: https://webpack.js.org/guides/production/
2+
* "src/development.ts" includes an additional check for development mode
3+
* which throws on multiple versions of graphql-js.
4+
*
5+
* This additional check will be included:
6+
* 1. if 'graphql/dev' is imported explicitly prior to all
7+
* other imports from this library, or
8+
* 2. if the "development" condition is set.
149
*/
15-
export const instanceOf: (value: unknown, constructor: Constructor) => boolean =
16-
/* c8 ignore next 6 */
17-
// FIXME: https://github.com/graphql/graphql-js/issues/2317
18-
isProduction
19-
? function instanceOf(value: unknown, constructor: Constructor): boolean {
20-
return value instanceof constructor;
21-
}
22-
: function instanceOf(value: unknown, constructor: Constructor): boolean {
23-
if (value instanceof constructor) {
24-
return true;
25-
}
26-
if (typeof value === 'object' && value !== null) {
27-
// Prefer Symbol.toStringTag since it is immune to minification.
28-
const className = constructor.prototype[Symbol.toStringTag];
29-
const valueClassName =
30-
// We still need to support constructor's name to detect conflicts with older versions of this library.
31-
Symbol.toStringTag in value
32-
? value[Symbol.toStringTag]
33-
: value.constructor?.name;
34-
if (className === valueClassName) {
35-
const stringifiedValue = inspect(value);
36-
throw new Error(
37-
`Cannot use ${className} "${stringifiedValue}" from another module or realm.
38-
39-
Ensure that there is only one instance of "graphql" in the node_modules
40-
directory. If different versions of "graphql" are the dependencies of other
41-
relied on modules, use "resolutions" to ensure only one version is installed.
10+
const check: (_value: unknown, _constructor: Constructor) => void =
11+
(globalThis as any)[Symbol.for('graphql.instanceOfCheck')] ??
12+
((_value: unknown, _constructor: Constructor) => {
13+
/* no-op */
14+
});
4215

43-
https://yarnpkg.com/en/docs/selective-version-resolutions
44-
45-
Duplicate "graphql" modules cannot be used at the same time since different
46-
versions may have different capabilities and behavior. The data from one
47-
version used in the function from another could produce confusing and
48-
spurious results.`,
49-
);
50-
}
51-
}
52-
return false;
53-
};
16+
export function instanceOf(value: unknown, constructor: Constructor): boolean {
17+
if (value instanceof constructor) {
18+
return true;
19+
}
20+
check(value, constructor);
21+
return false;
22+
}
5423

55-
interface Constructor {
24+
export interface Constructor {
5625
prototype: {
5726
[Symbol.toStringTag]: string;
5827
};

0 commit comments

Comments
 (0)