Skip to content

Commit 6cd3d4d

Browse files
committed
use conditional imports instead of environment for instanceOf checks
1 parent 8b86cd2 commit 6cd3d4d

12 files changed

+235
-122
lines changed

package.json

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
"engines": {
2929
"node": "^16.19.0 || ^18.14.0 || >=19.7.0"
3030
},
31+
"imports": {
32+
"#instanceOf": {
33+
"development": "./src/jsutils/instanceOfForDevelopment.ts",
34+
"default": "./src/jsutils/instanceOf.ts"
35+
}
36+
},
3137
"scripts": {
3238
"preversion": "bash -c '. ./resources/checkgit.sh && npm ci --ignore-scripts'",
3339
"version": "node --loader ts-node/esm resources/gen-version.ts && npm test && git add src/version.ts",

resources/build-deno.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import ts from 'typescript';
55

66
import { changeExtensionInImportPaths } from './change-extension-in-import-paths.js';
77
import { inlineInvariant } from './inline-invariant.js';
8+
import type { ImportsMap } from './utils.js';
89
import {
910
prettify,
11+
readPackageJSON,
1012
readTSConfig,
1113
showDirStats,
1214
writeGeneratedFile,
@@ -15,7 +17,10 @@ import {
1517
fs.rmSync('./denoDist', { recursive: true, force: true });
1618
fs.mkdirSync('./denoDist');
1719

18-
const tsProgram = ts.createProgram(['src/index.ts'], readTSConfig());
20+
const tsProgram = ts.createProgram(
21+
['src/index.ts', 'src/jsutils/instanceOf.ts'],
22+
readTSConfig(),
23+
);
1924
for (const sourceFile of tsProgram.getSourceFiles()) {
2025
if (
2126
tsProgram.isSourceFileFromExternalLibrary(sourceFile) ||
@@ -45,4 +50,40 @@ for (const sourceFile of tsProgram.getSourceFiles()) {
4550
fs.copyFileSync('./LICENSE', './denoDist/LICENSE');
4651
fs.copyFileSync('./README.md', './denoDist/README.md');
4752

53+
const imports = getImports();
54+
const importsJsonPath = `./denoDist/imports.json`;
55+
const prettified = await prettify(importsJsonPath, JSON.stringify(imports));
56+
writeGeneratedFile(importsJsonPath, prettified);
57+
4858
showDirStats('./denoDist');
59+
60+
function getImports(): ImportsMap {
61+
const packageJSON = readPackageJSON();
62+
const newImports: ImportsMap = {};
63+
for (const [key, value] of Object.entries(packageJSON.imports)) {
64+
if (typeof value === 'string') {
65+
newImports[key] = updateImportPath(value, '.ts');
66+
continue;
67+
}
68+
const findCondition = findDefault(value);
69+
if (findCondition !== undefined) {
70+
newImports[key] = updateImportPath(findCondition, '.ts');
71+
}
72+
}
73+
return newImports;
74+
}
75+
76+
function updateImportPath(value: string, extension: string) {
77+
return value.replace(/\/src\//g, '/').replace(/\.ts$/, extension);
78+
}
79+
80+
function findDefault(importsMap: ImportsMap): string | undefined {
81+
for (const [key, value] of Object.entries(importsMap)) {
82+
if (key === 'deno' || key === 'default') {
83+
if (typeof value === 'string') {
84+
return value;
85+
}
86+
return findDefault(value);
87+
}
88+
}
89+
}

resources/build-npm.ts

+41-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ts from 'typescript';
66

77
import { changeExtensionInImportPaths } from './change-extension-in-import-paths.js';
88
import { inlineInvariant } from './inline-invariant.js';
9+
import type { ImportsMap } from './utils.js';
910
import {
1011
prettify,
1112
readPackageJSON,
@@ -102,12 +103,23 @@ async function buildPackage(outDir: string, isESMOnly: boolean): Promise<void> {
102103
packageJSON.exports['./*.js'] = './*.js';
103104
packageJSON.exports['./*'] = './*.js';
104105

106+
packageJSON.imports = mapImports(packageJSON.imports, (value: string) =>
107+
updateImportPath(value, '.js'),
108+
);
109+
110+
packageJSON.type = 'module';
105111
packageJSON.publishConfig.tag += '-esm';
106112
packageJSON.version += '+esm';
107113
} else {
108-
delete packageJSON.type;
114+
packageJSON.type = 'commonjs';
109115
packageJSON.main = 'index';
110116
packageJSON.module = 'index.mjs';
117+
118+
packageJSON.imports = mapImports(packageJSON.imports, (value: string) => ({
119+
import: updateImportPath(value, '.mjs'),
120+
default: updateImportPath(value, '.js'),
121+
}));
122+
111123
emitTSFiles({ outDir, module: 'commonjs', extension: '.js' });
112124
emitTSFiles({ outDir, module: 'es2020', extension: '.mjs' });
113125
}
@@ -121,6 +133,25 @@ async function buildPackage(outDir: string, isESMOnly: boolean): Promise<void> {
121133
writeGeneratedFile(packageJsonPath, prettified);
122134
}
123135

136+
function updateImportPath(value: string, extension: string) {
137+
return value.replace(/\/src\//g, '/').replace(/\.ts$/, extension);
138+
}
139+
140+
function mapImports(
141+
imports: ImportsMap,
142+
replacer: (value: string) => string | ImportsMap,
143+
): ImportsMap {
144+
const newImports: ImportsMap = {};
145+
for (const [key, value] of Object.entries(imports)) {
146+
if (typeof value === 'string') {
147+
newImports[key] = replacer(value);
148+
continue;
149+
}
150+
newImports[key] = mapImports(value, replacer);
151+
}
152+
return newImports;
153+
}
154+
124155
// Based on https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#getting-the-dts-from-a-javascript-file
125156
function emitTSFiles(options: {
126157
outDir: string;
@@ -143,7 +174,15 @@ function emitTSFiles(options: {
143174
tsHost.writeFile = (filepath, body) =>
144175
writeGeneratedFile(filepath.replace(/.js$/, extension), body);
145176

146-
const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost);
177+
const tsProgram = ts.createProgram(
178+
[
179+
'src/index.ts',
180+
'src/jsutils/instanceOf.ts',
181+
'src/jsutils/instanceOfForDevelopment.ts',
182+
],
183+
tsOptions,
184+
tsHost,
185+
);
147186
const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, {
148187
after: [changeExtensionInImportPaths({ extension }), inlineInvariant],
149188
});

resources/utils.ts

+5
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@ export function writeGeneratedFile(filepath: string, body: string): void {
227227
fs.writeFileSync(filepath, body);
228228
}
229229

230+
export interface ImportsMap {
231+
[path: string]: string | ImportsMap;
232+
}
233+
230234
interface PackageJSON {
231235
description: string;
232236
version: string;
@@ -235,6 +239,7 @@ interface PackageJSON {
235239
scripts?: { [name: string]: string };
236240
type?: string;
237241
exports: { [path: string]: string };
242+
imports: ImportsMap;
238243
types?: string;
239244
typesVersions: { [ranges: string]: { [path: string]: Array<string> } };
240245
devDependencies?: { [name: string]: string };

src/jsutils/__tests__/instanceOf-test.ts

+1-61
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe('instanceOf', () => {
77
it('do not throw on values without prototype', () => {
88
class Foo {
99
get [Symbol.toStringTag]() {
10+
/* c8 ignore next 2 */
1011
return 'Foo';
1112
}
1213
}
@@ -15,65 +16,4 @@ describe('instanceOf', () => {
1516
expect(instanceOf(null, Foo)).to.equal(false);
1617
expect(instanceOf(Object.create(null), Foo)).to.equal(false);
1718
});
18-
19-
it('detect name clashes with older versions of this lib', () => {
20-
function oldVersion() {
21-
class Foo {}
22-
return Foo;
23-
}
24-
25-
function newVersion() {
26-
class Foo {
27-
get [Symbol.toStringTag]() {
28-
return 'Foo';
29-
}
30-
}
31-
return Foo;
32-
}
33-
34-
const NewClass = newVersion();
35-
const OldClass = oldVersion();
36-
expect(instanceOf(new NewClass(), NewClass)).to.equal(true);
37-
expect(() => instanceOf(new OldClass(), NewClass)).to.throw();
38-
});
39-
40-
it('allows instances to have share the same constructor name', () => {
41-
function getMinifiedClass(tag: string) {
42-
class SomeNameAfterMinification {
43-
get [Symbol.toStringTag]() {
44-
return tag;
45-
}
46-
}
47-
return SomeNameAfterMinification;
48-
}
49-
50-
const Foo = getMinifiedClass('Foo');
51-
const Bar = getMinifiedClass('Bar');
52-
expect(instanceOf(new Foo(), Bar)).to.equal(false);
53-
expect(instanceOf(new Bar(), Foo)).to.equal(false);
54-
55-
const DuplicateOfFoo = getMinifiedClass('Foo');
56-
expect(() => instanceOf(new DuplicateOfFoo(), Foo)).to.throw();
57-
expect(() => instanceOf(new Foo(), DuplicateOfFoo)).to.throw();
58-
});
59-
60-
it('fails with descriptive error message', () => {
61-
function getFoo() {
62-
class Foo {
63-
get [Symbol.toStringTag]() {
64-
return 'Foo';
65-
}
66-
}
67-
return Foo;
68-
}
69-
const Foo1 = getFoo();
70-
const Foo2 = getFoo();
71-
72-
expect(() => instanceOf(new Foo1(), Foo2)).to.throw(
73-
/^Cannot use Foo "{}" from another module or realm./m,
74-
);
75-
expect(() => instanceOf(new Foo2(), Foo1)).to.throw(
76-
/^Cannot use Foo "{}" from another module or realm./m,
77-
);
78-
});
7919
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { instanceOf as instanceOfForDevelopment } from '../instanceOfForDevelopment.js';
5+
6+
describe('instanceOfForDevelopment', () => {
7+
it('do not throw on values without prototype', () => {
8+
class Foo {
9+
get [Symbol.toStringTag]() {
10+
return 'Foo';
11+
}
12+
}
13+
14+
expect(instanceOfForDevelopment(true, Foo)).to.equal(false);
15+
expect(instanceOfForDevelopment(null, Foo)).to.equal(false);
16+
expect(instanceOfForDevelopment(Object.create(null), Foo)).to.equal(false);
17+
});
18+
19+
it('detect name clashes with older versions of this lib', () => {
20+
function oldVersion() {
21+
class Foo {}
22+
return Foo;
23+
}
24+
25+
function newVersion() {
26+
class Foo {
27+
get [Symbol.toStringTag]() {
28+
return 'Foo';
29+
}
30+
}
31+
return Foo;
32+
}
33+
34+
const NewClass = newVersion();
35+
const OldClass = oldVersion();
36+
expect(instanceOfForDevelopment(new NewClass(), NewClass)).to.equal(true);
37+
expect(() => instanceOfForDevelopment(new OldClass(), NewClass)).to.throw();
38+
});
39+
40+
it('allows instances to have share the same constructor name', () => {
41+
function getMinifiedClass(tag: string) {
42+
class SomeNameAfterMinification {
43+
get [Symbol.toStringTag]() {
44+
return tag;
45+
}
46+
}
47+
return SomeNameAfterMinification;
48+
}
49+
50+
const Foo = getMinifiedClass('Foo');
51+
const Bar = getMinifiedClass('Bar');
52+
expect(instanceOfForDevelopment(new Foo(), Bar)).to.equal(false);
53+
expect(instanceOfForDevelopment(new Bar(), Foo)).to.equal(false);
54+
55+
const DuplicateOfFoo = getMinifiedClass('Foo');
56+
expect(() =>
57+
instanceOfForDevelopment(new DuplicateOfFoo(), Foo),
58+
).to.throw();
59+
expect(() =>
60+
instanceOfForDevelopment(new Foo(), DuplicateOfFoo),
61+
).to.throw();
62+
});
63+
64+
it('fails with descriptive error message', () => {
65+
function getFoo() {
66+
class Foo {
67+
get [Symbol.toStringTag]() {
68+
return 'Foo';
69+
}
70+
}
71+
return Foo;
72+
}
73+
const Foo1 = getFoo();
74+
const Foo2 = getFoo();
75+
76+
expect(() => instanceOfForDevelopment(new Foo1(), Foo2)).to.throw(
77+
/^Cannot use Foo "{}" from another module or realm./m,
78+
);
79+
expect(() => instanceOfForDevelopment(new Foo2(), Foo1)).to.throw(
80+
/^Cannot use Foo "{}" from another module or realm./m,
81+
);
82+
});
83+
});

src/jsutils/instanceOf.ts

+3-54
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,6 @@
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-
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-
};
54-
1+
export function instanceOf(value: unknown, constructor: Constructor): boolean {
2+
return value instanceof constructor;
3+
}
554
interface Constructor extends Function {
565
prototype: {
576
[Symbol.toStringTag]: string;

0 commit comments

Comments
 (0)