From b25f2eea5f5f8da05a5caba1538763f2fccbfc70 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 8 Oct 2024 22:41:10 +0300 Subject: [PATCH] use imports --- package.json | 6 ++ resources/build-deno.ts | 9 +- resources/build-npm.ts | 52 +++++++++--- resources/utils.ts | 7 ++ src/jsutils/__tests__/instanceOf-test.ts | 62 +------------- .../instanceOfForDevelopment-test.ts | 83 +++++++++++++++++++ src/jsutils/instanceOf.ts | 57 +------------ src/jsutils/instanceOfForDevelopment.ts | 46 ++++++++++ src/language/source.ts | 3 +- src/type/definition.ts | 3 +- src/type/directives.ts | 3 +- src/type/schema.ts | 3 +- 12 files changed, 203 insertions(+), 131 deletions(-) create mode 100644 src/jsutils/__tests__/instanceOfForDevelopment-test.ts create mode 100644 src/jsutils/instanceOfForDevelopment.ts 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 bb17d041d1..e528ac33b8 100644 --- a/resources/build-deno.ts +++ b/resources/build-deno.ts @@ -14,7 +14,14 @@ 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', + 'src/jsutils/instanceOfForDevelopment.ts', + ], + readTSConfig(), +); for (const sourceFile of tsProgram.getSourceFiles()) { if ( tsProgram.isSourceFileFromExternalLibrary(sourceFile) || diff --git a/resources/build-npm.ts b/resources/build-npm.ts index 01413373b6..19f742e8a6 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.ts'; import { inlineInvariant } from './inline-invariant.ts'; +import type { ImportsMap, PathOrConditionalPath } from './utils.ts'; import { prettify, readPackageJSON, @@ -87,7 +88,6 @@ async function buildPackage(outDir: string, isESMOnly: boolean): Promise { const { emittedTSFiles } = emitTSFiles({ outDir, - module: 'es2020', extension: '.js', }); @@ -102,14 +102,24 @@ async function buildPackage(outDir: string, isESMOnly: boolean): Promise { packageJSON.exports['./*.js'] = './*.ts'; packageJSON.exports['./*'] = './*.ts'; + rewriteImports(packageJSON.imports, (value: string) => + replaceImportPath(value, '.js'), + ); + packageJSON.publishConfig.tag += '-esm'; packageJSON.version += '+esm'; } else { delete packageJSON.type; packageJSON.main = 'index'; packageJSON.module = 'index.mjs'; - emitTSFiles({ outDir, module: 'commonjs', extension: '.js' }); - emitTSFiles({ outDir, module: 'es2020', extension: '.mjs' }); + + rewriteImports(packageJSON.imports, (value: string) => ({ + import: replaceImportPath(value, '.mjs'), + default: replaceImportPath(value, '.js'), + })); + + emitTSFiles({ outDir, extension: '.js' }); + emitTSFiles({ outDir, extension: '.mjs' }); } const packageJsonPath = `./${outDir}/package.json`; @@ -121,17 +131,29 @@ async function buildPackage(outDir: string, isESMOnly: boolean): Promise { writeGeneratedFile(packageJsonPath, prettified); } +function replaceImportPath(value: string, extension: string) { + return value.replace(/\/src\//g, '/').replace(/\.ts$/, extension); +} + +function rewriteImports( + imports: ImportsMap, + replacer: (value: string) => PathOrConditionalPath, +) { + for (const [key, value] of Object.entries(imports)) { + if (typeof value === 'string') { + imports[key] = replacer(value); + continue; + } + rewriteImports(value, replacer); + } +} + // Based on https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#getting-the-dts-from-a-javascript-file -function emitTSFiles(options: { - outDir: string; - module: string; - extension: string; -}): { +function emitTSFiles(options: { outDir: string; extension: string }): { emittedTSFiles: ReadonlyArray; } { - const { outDir, module, extension } = options; + const { outDir, extension } = options; const tsOptions = readTSConfig({ - module, noEmit: false, declaration: true, declarationDir: outDir, @@ -143,7 +165,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, { before: [changeExtensionInImportPaths({ extension }), inlineInvariant], }); diff --git a/resources/utils.ts b/resources/utils.ts index 4291ddc20a..03adb61c6d 100644 --- a/resources/utils.ts +++ b/resources/utils.ts @@ -227,6 +227,12 @@ export function writeGeneratedFile(filepath: string, body: string): void { fs.writeFileSync(filepath, body); } +export type PathOrConditionalPath = string | { [condition: string]: string }; + +export interface ImportsMap { + [path: string]: PathOrConditionalPath; +} + interface PackageJSON { description: string; version: string; @@ -235,6 +241,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 187ad4506f..1447de11dc 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..ece26bb3eb --- /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.ts'; + +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 7a5ed77712..0f893a016b 100644 --- a/src/jsutils/instanceOf.ts +++ b/src/jsutils/instanceOf.ts @@ -1,57 +1,6 @@ -import { inspect } from './inspect.ts'; - -/* 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..3b57e9abd9 --- /dev/null +++ b/src/jsutils/instanceOfForDevelopment.ts @@ -0,0 +1,46 @@ +import { inspect } from './inspect.ts'; + +/** + * 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 d4c4047768..1e9a9c4cd3 100644 --- a/src/language/source.ts +++ b/src/language/source.ts @@ -1,5 +1,6 @@ import { devAssert } from '../jsutils/devAssert.ts'; -import { instanceOf } from '../jsutils/instanceOf.ts'; + +import { instanceOf } from '#instanceOf'; interface Location { line: number; diff --git a/src/type/definition.ts b/src/type/definition.ts index 62aca65446..e51d09fef5 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -2,7 +2,6 @@ import { devAssert } from '../jsutils/devAssert.ts'; import { didYouMean } from '../jsutils/didYouMean.ts'; import { identityFunc } from '../jsutils/identityFunc.ts'; import { inspect } from '../jsutils/inspect.ts'; -import { instanceOf } from '../jsutils/instanceOf.ts'; import { keyMap } from '../jsutils/keyMap.ts'; import { keyValMap } from '../jsutils/keyValMap.ts'; import { mapValue } from '../jsutils/mapValue.ts'; @@ -47,6 +46,8 @@ import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped.ts'; import { assertEnumValueName, assertName } from './assertName.ts'; import type { GraphQLSchema } from './schema.ts'; +import { instanceOf } from '#instanceOf'; + // Predicates & Assertions /** diff --git a/src/type/directives.ts b/src/type/directives.ts index fbef07d6be..da681e57d8 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -1,5 +1,4 @@ import { inspect } from '../jsutils/inspect.ts'; -import { instanceOf } from '../jsutils/instanceOf.ts'; import type { Maybe } from '../jsutils/Maybe.ts'; import { toObjMap } from '../jsutils/toObjMap.ts'; @@ -18,6 +17,8 @@ import { } from './definition.ts'; import { GraphQLBoolean, GraphQLInt, GraphQLString } from './scalars.ts'; +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 2efccffb37..8c2a99328e 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -1,5 +1,4 @@ import { inspect } from '../jsutils/inspect.ts'; -import { instanceOf } from '../jsutils/instanceOf.ts'; import type { Maybe } from '../jsutils/Maybe.ts'; import type { ObjMap } from '../jsutils/ObjMap.ts'; import { toObjMap } from '../jsutils/toObjMap.ts'; @@ -37,6 +36,8 @@ import { TypeNameMetaFieldDef, } from './introspection.ts'; +import { instanceOf } from '#instanceOf'; + /** * Test if the given value is a GraphQL schema. */