diff --git a/package.json b/package.json index f0de106263..49db3e75f4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,12 @@ "engines": { "node": "^16.19.0 || ^18.14.0 || >=19.7.0" }, + "imports": { + "#instanceOf": { + "development": "./src/jsutils/instanceOfForDevelopment.ts", + "default": "./src/jsutils/instanceOf.ts" + } + }, "scripts": { "preversion": "bash -c '. ./resources/checkgit.sh && npm ci --ignore-scripts'", "version": "node --loader ts-node/esm resources/gen-version.ts && npm test && git add src/version.ts", diff --git a/resources/build-deno.ts b/resources/build-deno.ts index 43a4b292b1..f47150808a 100644 --- a/resources/build-deno.ts +++ b/resources/build-deno.ts @@ -5,8 +5,10 @@ import ts from 'typescript'; import { changeExtensionInImportPaths } from './change-extension-in-import-paths.js'; import { inlineInvariant } from './inline-invariant.js'; +import type { ImportsMap } from './utils.js'; import { prettify, + readPackageJSON, readTSConfig, showDirStats, writeGeneratedFile, @@ -15,7 +17,10 @@ import { fs.rmSync('./denoDist', { recursive: true, force: true }); fs.mkdirSync('./denoDist'); -const tsProgram = ts.createProgram(['src/index.ts'], readTSConfig()); +const tsProgram = ts.createProgram( + ['src/index.ts', 'src/jsutils/instanceOf.ts'], + readTSConfig(), +); for (const sourceFile of tsProgram.getSourceFiles()) { if ( tsProgram.isSourceFileFromExternalLibrary(sourceFile) || @@ -45,4 +50,40 @@ for (const sourceFile of tsProgram.getSourceFiles()) { fs.copyFileSync('./LICENSE', './denoDist/LICENSE'); fs.copyFileSync('./README.md', './denoDist/README.md'); +const imports = getImports(); +const importsJsonPath = `./denoDist/imports.json`; +const prettified = await prettify(importsJsonPath, JSON.stringify(imports)); +writeGeneratedFile(importsJsonPath, prettified); + showDirStats('./denoDist'); + +function getImports(): ImportsMap { + const packageJSON = readPackageJSON(); + const newImports: ImportsMap = {}; + for (const [key, value] of Object.entries(packageJSON.imports)) { + if (typeof value === 'string') { + newImports[key] = updateImportPath(value, '.ts'); + continue; + } + const findCondition = findDefault(value); + if (findCondition !== undefined) { + newImports[key] = updateImportPath(findCondition, '.ts'); + } + } + return newImports; +} + +function updateImportPath(value: string, extension: string) { + return value.replace(/\/src\//g, '/').replace(/\.ts$/, extension); +} + +function findDefault(importsMap: ImportsMap): string | undefined { + for (const [key, value] of Object.entries(importsMap)) { + if (key === 'deno' || key === 'default') { + if (typeof value === 'string') { + return value; + } + return findDefault(value); + } + } +} diff --git a/resources/build-npm.ts b/resources/build-npm.ts index a68ce9bbe7..cf9d962709 100644 --- a/resources/build-npm.ts +++ b/resources/build-npm.ts @@ -6,6 +6,7 @@ import ts from 'typescript'; import { changeExtensionInImportPaths } from './change-extension-in-import-paths.js'; import { inlineInvariant } from './inline-invariant.js'; +import type { ImportsMap } from './utils.js'; import { prettify, readPackageJSON, @@ -102,12 +103,23 @@ async function buildPackage(outDir: string, isESMOnly: boolean): Promise { packageJSON.exports['./*.js'] = './*.js'; packageJSON.exports['./*'] = './*.js'; + packageJSON.imports = mapImports(packageJSON.imports, (value: string) => + updateImportPath(value, '.js'), + ); + + packageJSON.type = 'module'; packageJSON.publishConfig.tag += '-esm'; packageJSON.version += '+esm'; } else { - delete packageJSON.type; + packageJSON.type = 'commonjs'; packageJSON.main = 'index'; packageJSON.module = 'index.mjs'; + + packageJSON.imports = mapImports(packageJSON.imports, (value: string) => ({ + import: updateImportPath(value, '.mjs'), + default: updateImportPath(value, '.js'), + })); + emitTSFiles({ outDir, module: 'commonjs', extension: '.js' }); emitTSFiles({ outDir, module: 'es2020', extension: '.mjs' }); } @@ -121,6 +133,25 @@ async function buildPackage(outDir: string, isESMOnly: boolean): Promise { writeGeneratedFile(packageJsonPath, prettified); } +function updateImportPath(value: string, extension: string) { + return value.replace(/\/src\//g, '/').replace(/\.ts$/, extension); +} + +function mapImports( + imports: ImportsMap, + replacer: (value: string) => string | ImportsMap, +): ImportsMap { + const newImports: ImportsMap = {}; + for (const [key, value] of Object.entries(imports)) { + if (typeof value === 'string') { + newImports[key] = replacer(value); + continue; + } + newImports[key] = mapImports(value, replacer); + } + return newImports; +} + // Based on https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#getting-the-dts-from-a-javascript-file function emitTSFiles(options: { outDir: string; @@ -143,7 +174,15 @@ function emitTSFiles(options: { tsHost.writeFile = (filepath, body) => writeGeneratedFile(filepath.replace(/.js$/, extension), body); - const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost); + const tsProgram = ts.createProgram( + [ + 'src/index.ts', + 'src/jsutils/instanceOf.ts', + 'src/jsutils/instanceOfForDevelopment.ts', + ], + tsOptions, + tsHost, + ); const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, { after: [changeExtensionInImportPaths({ extension }), inlineInvariant], }); diff --git a/resources/utils.ts b/resources/utils.ts index 4291ddc20a..5c5cc570f4 100644 --- a/resources/utils.ts +++ b/resources/utils.ts @@ -227,6 +227,10 @@ export function writeGeneratedFile(filepath: string, body: string): void { fs.writeFileSync(filepath, body); } +export interface ImportsMap { + [path: string]: string | ImportsMap; +} + interface PackageJSON { description: string; version: string; @@ -235,6 +239,7 @@ interface PackageJSON { scripts?: { [name: string]: string }; type?: string; exports: { [path: string]: string }; + imports: ImportsMap; types?: string; typesVersions: { [ranges: string]: { [path: string]: Array } }; devDependencies?: { [name: string]: string }; diff --git a/src/jsutils/__tests__/instanceOf-test.ts b/src/jsutils/__tests__/instanceOf-test.ts index 5a54a641e5..1a5ee2b3e6 100644 --- a/src/jsutils/__tests__/instanceOf-test.ts +++ b/src/jsutils/__tests__/instanceOf-test.ts @@ -7,6 +7,7 @@ describe('instanceOf', () => { it('do not throw on values without prototype', () => { class Foo { get [Symbol.toStringTag]() { + /* c8 ignore next 2 */ return 'Foo'; } } @@ -15,65 +16,4 @@ describe('instanceOf', () => { expect(instanceOf(null, Foo)).to.equal(false); expect(instanceOf(Object.create(null), Foo)).to.equal(false); }); - - it('detect name clashes with older versions of this lib', () => { - function oldVersion() { - class Foo {} - return Foo; - } - - function newVersion() { - class Foo { - get [Symbol.toStringTag]() { - return 'Foo'; - } - } - return Foo; - } - - const NewClass = newVersion(); - const OldClass = oldVersion(); - expect(instanceOf(new NewClass(), NewClass)).to.equal(true); - expect(() => instanceOf(new OldClass(), NewClass)).to.throw(); - }); - - it('allows instances to have share the same constructor name', () => { - function getMinifiedClass(tag: string) { - class SomeNameAfterMinification { - get [Symbol.toStringTag]() { - return tag; - } - } - return SomeNameAfterMinification; - } - - const Foo = getMinifiedClass('Foo'); - const Bar = getMinifiedClass('Bar'); - expect(instanceOf(new Foo(), Bar)).to.equal(false); - expect(instanceOf(new Bar(), Foo)).to.equal(false); - - const DuplicateOfFoo = getMinifiedClass('Foo'); - expect(() => instanceOf(new DuplicateOfFoo(), Foo)).to.throw(); - expect(() => instanceOf(new Foo(), DuplicateOfFoo)).to.throw(); - }); - - it('fails with descriptive error message', () => { - function getFoo() { - class Foo { - get [Symbol.toStringTag]() { - return 'Foo'; - } - } - return Foo; - } - const Foo1 = getFoo(); - const Foo2 = getFoo(); - - expect(() => instanceOf(new Foo1(), Foo2)).to.throw( - /^Cannot use Foo "{}" from another module or realm./m, - ); - expect(() => instanceOf(new Foo2(), Foo1)).to.throw( - /^Cannot use Foo "{}" from another module or realm./m, - ); - }); }); diff --git a/src/jsutils/__tests__/instanceOfForDevelopment-test.ts b/src/jsutils/__tests__/instanceOfForDevelopment-test.ts new file mode 100644 index 0000000000..473ea637f2 --- /dev/null +++ b/src/jsutils/__tests__/instanceOfForDevelopment-test.ts @@ -0,0 +1,83 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { instanceOf as instanceOfForDevelopment } from '../instanceOfForDevelopment.js'; + +describe('instanceOfForDevelopment', () => { + it('do not throw on values without prototype', () => { + class Foo { + get [Symbol.toStringTag]() { + return 'Foo'; + } + } + + expect(instanceOfForDevelopment(true, Foo)).to.equal(false); + expect(instanceOfForDevelopment(null, Foo)).to.equal(false); + expect(instanceOfForDevelopment(Object.create(null), Foo)).to.equal(false); + }); + + it('detect name clashes with older versions of this lib', () => { + function oldVersion() { + class Foo {} + return Foo; + } + + function newVersion() { + class Foo { + get [Symbol.toStringTag]() { + return 'Foo'; + } + } + return Foo; + } + + const NewClass = newVersion(); + const OldClass = oldVersion(); + expect(instanceOfForDevelopment(new NewClass(), NewClass)).to.equal(true); + expect(() => instanceOfForDevelopment(new OldClass(), NewClass)).to.throw(); + }); + + it('allows instances to have share the same constructor name', () => { + function getMinifiedClass(tag: string) { + class SomeNameAfterMinification { + get [Symbol.toStringTag]() { + return tag; + } + } + return SomeNameAfterMinification; + } + + const Foo = getMinifiedClass('Foo'); + const Bar = getMinifiedClass('Bar'); + expect(instanceOfForDevelopment(new Foo(), Bar)).to.equal(false); + expect(instanceOfForDevelopment(new Bar(), Foo)).to.equal(false); + + const DuplicateOfFoo = getMinifiedClass('Foo'); + expect(() => + instanceOfForDevelopment(new DuplicateOfFoo(), Foo), + ).to.throw(); + expect(() => + instanceOfForDevelopment(new Foo(), DuplicateOfFoo), + ).to.throw(); + }); + + it('fails with descriptive error message', () => { + function getFoo() { + class Foo { + get [Symbol.toStringTag]() { + return 'Foo'; + } + } + return Foo; + } + const Foo1 = getFoo(); + const Foo2 = getFoo(); + + expect(() => instanceOfForDevelopment(new Foo1(), Foo2)).to.throw( + /^Cannot use Foo "{}" from another module or realm./m, + ); + expect(() => instanceOfForDevelopment(new Foo2(), Foo1)).to.throw( + /^Cannot use Foo "{}" from another module or realm./m, + ); + }); +}); diff --git a/src/jsutils/instanceOf.ts b/src/jsutils/instanceOf.ts index 562aee3e2f..0f893a016b 100644 --- a/src/jsutils/instanceOf.ts +++ b/src/jsutils/instanceOf.ts @@ -1,57 +1,6 @@ -import { inspect } from './inspect.js'; - -/* c8 ignore next 3 */ -const isProduction = - globalThis.process != null && - // eslint-disable-next-line no-undef - process.env.NODE_ENV === 'production'; - -/** - * A replacement for instanceof which includes an error warning when multi-realm - * constructors are detected. - * See: https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production - * See: https://webpack.js.org/guides/production/ - */ -export const instanceOf: (value: unknown, constructor: Constructor) => boolean = - /* c8 ignore next 6 */ - // FIXME: https://github.com/graphql/graphql-js/issues/2317 - isProduction - ? function instanceOf(value: unknown, constructor: Constructor): boolean { - return value instanceof constructor; - } - : function instanceOf(value: unknown, constructor: Constructor): boolean { - if (value instanceof constructor) { - return true; - } - if (typeof value === 'object' && value !== null) { - // Prefer Symbol.toStringTag since it is immune to minification. - const className = constructor.prototype[Symbol.toStringTag]; - const valueClassName = - // We still need to support constructor's name to detect conflicts with older versions of this library. - Symbol.toStringTag in value - ? value[Symbol.toStringTag] - : value.constructor?.name; - if (className === valueClassName) { - const stringifiedValue = inspect(value); - throw new Error( - `Cannot use ${className} "${stringifiedValue}" 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; - }; - +export function instanceOf(value: unknown, constructor: Constructor): boolean { + return value instanceof constructor; +} interface Constructor extends Function { prototype: { [Symbol.toStringTag]: string; diff --git a/src/jsutils/instanceOfForDevelopment.ts b/src/jsutils/instanceOfForDevelopment.ts new file mode 100644 index 0000000000..289ffcfcbb --- /dev/null +++ b/src/jsutils/instanceOfForDevelopment.ts @@ -0,0 +1,46 @@ +import { inspect } from './inspect.js'; + +/** + * A replacement for instanceof which includes an error warning when multi-realm + * constructors are detected. + * See: https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production + * See: https://webpack.js.org/guides/production/ + */ +export function instanceOf(value: unknown, constructor: Constructor): boolean { + if (value instanceof constructor) { + return true; + } + if (typeof value === 'object' && value !== null) { + // Prefer Symbol.toStringTag since it is immune to minification. + const className = constructor.prototype[Symbol.toStringTag]; + const valueClassName = + // We still need to support constructor's name to detect conflicts with older versions of this library. + Symbol.toStringTag in value + ? value[Symbol.toStringTag] + : value.constructor?.name; + if (className === valueClassName) { + const stringifiedValue = inspect(value); + throw new Error( + `Cannot use ${className} "${stringifiedValue}" 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; +} + +interface Constructor extends Function { + prototype: { + [Symbol.toStringTag]: string; + }; +} diff --git a/src/language/source.ts b/src/language/source.ts index eb21547154..a16fd759d5 100644 --- a/src/language/source.ts +++ b/src/language/source.ts @@ -1,5 +1,6 @@ import { devAssert } from '../jsutils/devAssert.js'; -import { instanceOf } from '../jsutils/instanceOf.js'; + +import { instanceOf } from '#instanceOf'; interface Location { line: number; diff --git a/src/type/definition.ts b/src/type/definition.ts index f00e0d5694..6007992686 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -2,7 +2,6 @@ import { devAssert } from '../jsutils/devAssert.js'; import { didYouMean } from '../jsutils/didYouMean.js'; import { identityFunc } from '../jsutils/identityFunc.js'; import { inspect } from '../jsutils/inspect.js'; -import { instanceOf } from '../jsutils/instanceOf.js'; import { keyMap } from '../jsutils/keyMap.js'; import { keyValMap } from '../jsutils/keyValMap.js'; import { mapValue } from '../jsutils/mapValue.js'; @@ -47,6 +46,8 @@ import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped.js'; import { assertEnumValueName, assertName } from './assertName.js'; import type { GraphQLSchema } from './schema.js'; +import { instanceOf } from '#instanceOf'; + // Predicates & Assertions /** diff --git a/src/type/directives.ts b/src/type/directives.ts index 48e90c5531..a3f1ea9b57 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -1,5 +1,4 @@ import { inspect } from '../jsutils/inspect.js'; -import { instanceOf } from '../jsutils/instanceOf.js'; import type { Maybe } from '../jsutils/Maybe.js'; import { toObjMap } from '../jsutils/toObjMap.js'; @@ -18,6 +17,8 @@ import { } from './definition.js'; import { GraphQLBoolean, GraphQLInt, GraphQLString } from './scalars.js'; +import { instanceOf } from '#instanceOf'; + /** * Test if the given value is a GraphQL directive. */ diff --git a/src/type/schema.ts b/src/type/schema.ts index 694454fae5..fee104b284 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -1,5 +1,4 @@ import { inspect } from '../jsutils/inspect.js'; -import { instanceOf } from '../jsutils/instanceOf.js'; import type { Maybe } from '../jsutils/Maybe.js'; import type { ObjMap } from '../jsutils/ObjMap.js'; import { toObjMap } from '../jsutils/toObjMap.js'; @@ -37,6 +36,8 @@ import { TypeNameMetaFieldDef, } from './introspection.js'; +import { instanceOf } from '#instanceOf'; + /** * Test if the given value is a GraphQL schema. */