Skip to content

Commit e39d481

Browse files
committed
use conditional imports to trigger development instanceOf()
Changes this library to use the development version of instanceOf when a condition is set rather than based on a Node JS specific environment variable.
1 parent 8b86cd2 commit e39d481

17 files changed

+302
-124
lines changed

Diff for: integrationTests/node/index.cjs

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { readFileSync } = require('fs');
44
const {
55
experimentalExecuteIncrementally,
66
graphqlSync,
7+
isSchema,
78
parse,
89
} = require('graphql');
910
const { buildSchema } = require('graphql/utilities');
@@ -52,3 +53,12 @@ result = experimentalExecuteIncrementally({
5253

5354
assert(result.errors?.[0] !== undefined);
5455
assert(!result.errors[0].message.includes('is not defined'));
56+
57+
// test instanceOf without development condition
58+
class GraphQLSchema {
59+
get [Symbol.toStringTag]() {
60+
return 'GraphQLSchema';
61+
}
62+
}
63+
const notSchema = new GraphQLSchema();
64+
assert(isSchema(notSchema) === false);

Diff for: integrationTests/node/index.mjs

+10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { readFileSync } from 'fs';
55
import {
66
experimentalExecuteIncrementally,
77
graphqlSync,
8+
isSchema,
89
parse,
910
} from 'graphql-esm';
1011
import { buildSchema } from 'graphql-esm/utilities';
@@ -53,3 +54,12 @@ result = experimentalExecuteIncrementally({
5354

5455
assert(result.errors?.[0] !== undefined);
5556
assert(!result.errors[0].message.includes('is not defined'));
57+
58+
// test instanceOf without development condition
59+
class GraphQLSchema {
60+
get [Symbol.toStringTag]() {
61+
return 'Source';
62+
}
63+
}
64+
const notSchema = new GraphQLSchema();
65+
assert(isSchema(notSchema) === false);

Diff for: integrationTests/node/instanceOfForDevelopment.cjs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const assert = require('assert');
2+
3+
const { isSchema } = require('graphql');
4+
5+
class GraphQLSchema {
6+
get [Symbol.toStringTag]() {
7+
return 'GraphQLSchema';
8+
}
9+
}
10+
const notSchema = new GraphQLSchema();
11+
let error;
12+
try {
13+
isSchema(notSchema);
14+
} catch (_error) {
15+
error = _error;
16+
}
17+
assert(
18+
String(error?.message).match(
19+
/^Cannot use GraphQLSchema "{}" from another module or realm./m,
20+
),
21+
);

Diff for: integrationTests/node/instanceOfForDevelopment.mjs

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* eslint-disable simple-import-sort/imports */
2+
import assert from 'assert';
3+
4+
import { isSchema } from 'graphql-esm';
5+
6+
class GraphQLSchema {
7+
get [Symbol.toStringTag]() {
8+
return 'GraphQLSchema';
9+
}
10+
}
11+
const notSchema = new GraphQLSchema();
12+
let error;
13+
try {
14+
isSchema(notSchema);
15+
} catch (_error) {
16+
error = _error;
17+
}
18+
assert(
19+
String(error?.message).match(
20+
/^Cannot use GraphQLSchema "{}" from another module or realm./m,
21+
),
22+
);

Diff for: integrationTests/node/test.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ const nodeVersions = graphqlPackageJSON.engines.node
1414
for (const version of nodeVersions) {
1515
console.log(`Testing on node@${version} ...`);
1616

17+
childProcess.execSync(`node ./index.cjs`, { stdio: 'inherit' });
1718
childProcess.execSync(
18-
`docker run --rm --volume "$PWD":/usr/src/app -w /usr/src/app node:${version}-slim node ./index.cjs`,
19+
`node --conditions=development ./instanceOfForDevelopment.cjs`,
1920
{ stdio: 'inherit' },
2021
);
2122

23+
childProcess.execSync(`node ./index.mjs`, { stdio: 'inherit' });
2224
childProcess.execSync(
23-
`docker run --rm --volume "$PWD":/usr/src/app -w /usr/src/app node:${version}-slim node ./index.mjs`,
25+
`node --conditions=development ./instanceOfForDevelopment.mjs`,
2426
{ stdio: 'inherit' },
2527
);
2628
}

Diff for: 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",

Diff for: 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 denoValue = findDenoValue(value);
69+
if (denoValue !== undefined) {
70+
newImports[key] = updateImportPath(denoValue, '.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 findDenoValue(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 findDenoValue(value);
87+
}
88+
}
89+
}

Diff for: 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
});

Diff for: 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 };

Diff for: 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
});

0 commit comments

Comments
 (0)