Skip to content

Commit b641982

Browse files
committed
use dev/prod conditions to set instanceOf check
1 parent ae18fca commit b641982

File tree

5 files changed

+127
-64
lines changed

5 files changed

+127
-64
lines changed

resources/build-npm.ts

Lines changed: 55 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,55 @@ 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+
const devHelper = path.join(outDir, 'development.js');
92+
93+
for (const prodFile of emittedTSFiles) {
94+
const { dir, base } = path.parse(prodFile);
95+
96+
const match = base.match(/^([^.]*)\.?(.*)$/);
97+
assert(match);
98+
const [, name, ext] = match;
99+
100+
if (prodFile === devHelper || ext === 'js.map') {
101+
continue;
102+
}
103+
104+
const relativePathToProd = crossPlatformRelativePath(prodFile, outDir);
105+
const relativePathAndName = crossPlatformRelativePath(
106+
outDir,
107+
`${dir}/${name}`,
108+
);
109+
110+
const lines =
111+
ext === 'd.ts' ? [] : [`import '${relativePathToProd}/development.js';`];
112+
lines.push(
113+
`export * from '${relativePathToProd}/${relativePathAndName}.js';`,
114+
);
115+
const body = lines.join('\n');
116+
117+
writeGeneratedFile(
118+
path.join(devDir, path.relative(outDir, prodFile)),
119+
body,
120+
);
121+
122+
if (base === 'index.js') {
123+
const dirname = path.dirname(relativePathAndName);
124+
packageJSON.exports[dirname === '.' ? dirname : `./${dirname}`] = {
125+
development: `./dev/${relativePathAndName}.js`,
126+
default: `./${relativePathAndName}.js`,
127+
};
93128
}
94129
}
95130

96131
// Temporary workaround to allow "internal" imports, no grantees provided
97-
packageJSON.exports['./*.js'] = './*.js';
98-
packageJSON.exports['./*'] = './*.js';
132+
packageJSON.exports['./*.js'] = {
133+
development: './dev/*.js',
134+
default: './*.js',
135+
};
136+
packageJSON.exports['./*'] = {
137+
development: './dev/*.js',
138+
default: './*.js',
139+
};
99140

100141
const packageJsonPath = `./${outDir}/package.json`;
101142
const prettified = await prettify(
@@ -127,7 +168,11 @@ function emitTSFiles(options: {
127168
const tsHost = ts.createCompilerHost(tsOptions);
128169
tsHost.writeFile = (filepath, body) => writeGeneratedFile(filepath, body);
129170

130-
const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost);
171+
const tsProgram = ts.createProgram(
172+
['src/index.ts', 'src/development.ts'],
173+
tsOptions,
174+
tsHost,
175+
);
131176
const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, {
132177
after: [changeExtensionInImportPaths({ extension }), inlineInvariant],
133178
});

resources/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,12 @@ interface PackageJSON {
234234
repository?: { url?: string };
235235
scripts?: { [name: string]: string };
236236
type?: string;
237-
exports: { [path: string]: string };
237+
exports: {
238+
[path: string]: {
239+
development: string;
240+
default: string;
241+
};
242+
};
238243
types?: string;
239244
typesVersions: { [ranges: string]: { [path: string]: Array<string> } };
240245
devDependencies?: { [name: string]: string };

src/development.ts

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

src/jsutils/__tests__/instanceOf-test.ts

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

4-
import { instanceOf } from '../instanceOf.js';
4+
import { developmentInstanceOfCheck } from '../../development.js';
5+
6+
import { symbolForGraphQLInstanceOfCheck } from '../instanceOf.js';
7+
8+
(globalThis as any)[symbolForGraphQLInstanceOfCheck] =
9+
developmentInstanceOfCheck;
10+
11+
const { instanceOf } = await import(`../instanceOf.js?ts${Date.now()}`);
512

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

src/jsutils/instanceOf.ts

Lines changed: 16 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,22 @@
1-
import { inspect } from './inspect.js';
1+
export const symbolForGraphQLInstanceOfCheck = Symbol.for(
2+
'graphql.instanceOfCheck',
3+
);
24

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';
5+
const check: (_value: unknown, _constructor: Constructor) => void =
6+
(globalThis as any)[symbolForGraphQLInstanceOfCheck] ??
7+
((_value: unknown, _constructor: Constructor) => {
8+
/* no-op */
9+
});
810

9-
/**
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/
14-
*/
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.
42-
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-
};
11+
export function instanceOf(value: unknown, constructor: Constructor): boolean {
12+
if (value instanceof constructor) {
13+
return true;
14+
}
15+
check(value, constructor);
16+
return false;
17+
}
5418

55-
interface Constructor {
19+
export interface Constructor {
5620
prototype: {
5721
[Symbol.toStringTag]: string;
5822
};

0 commit comments

Comments
 (0)