diff --git a/packages/experimental-utils/package.json b/packages/experimental-utils/package.json index 690771a841b8..97c0bc1f5ef3 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -44,13 +44,23 @@ "@typescript-eslint/types": "4.28.5", "@typescript-eslint/typescript-estree": "4.28.5", "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0", - "tsutils": "^3.21.0" + "eslint-utils": "^3.0.0" }, "peerDependencies": { - "eslint": "*" + "eslint": "*", + "tsutils": "^3.21.0", + "typescript": "^3.4.1 || ^4.0.0" + }, + "peerDependenciesMeta": { + "tsutils": { + "optional": true + }, + "typescript": { + "optional": true + } }, "devDependencies": { + "tsutils": "*", "typescript": "*" }, "funding": { diff --git a/packages/experimental-utils/src/conditional-imports/tsutils.ts b/packages/experimental-utils/src/conditional-imports/tsutils.ts new file mode 100644 index 000000000000..9cd99d19aca6 --- /dev/null +++ b/packages/experimental-utils/src/conditional-imports/tsutils.ts @@ -0,0 +1,12 @@ +import type * as tsutils from 'tsutils'; + +// Conditionally loaded tsutils but only if it is available. +const conditionalTsutils = ((): typeof tsutils | Error => { + try { + return require('tsutils') as typeof tsutils; + } catch { + return new Error('Cannot find local tsutils peer depenancy.'); + } +})(); + +export { conditionalTsutils }; diff --git a/packages/experimental-utils/src/conditional-imports/typescript.ts b/packages/experimental-utils/src/conditional-imports/typescript.ts new file mode 100644 index 000000000000..12f8d9d89dc3 --- /dev/null +++ b/packages/experimental-utils/src/conditional-imports/typescript.ts @@ -0,0 +1,12 @@ +import type * as ts from 'typescript'; + +// Conditionally loaded TypeScript but only if it is available. +const conditionalTypeScript = ((): typeof ts | Error => { + try { + return require('typescript') as typeof ts; + } catch { + return new Error('Cannot find local typescript peer depenancy.'); + } +})(); + +export { conditionalTypeScript }; diff --git a/packages/experimental-utils/src/eslint-utils/isTypeReadonly.ts b/packages/experimental-utils/src/eslint-utils/isTypeReadonly.ts index b10f00006ec5..399d3bea45d9 100644 --- a/packages/experimental-utils/src/eslint-utils/isTypeReadonly.ts +++ b/packages/experimental-utils/src/eslint-utils/isTypeReadonly.ts @@ -1,11 +1,9 @@ -import { - isObjectType, - isUnionType, - isUnionOrIntersectionType, - unionTypeParts, - isPropertyReadonlyInType, -} from 'tsutils'; -import * as ts from 'typescript'; +import assert from 'assert'; +import type * as ts from 'typescript'; + +import { conditionalTypeScript } from '../conditional-imports/typescript'; +import { conditionalTsutils } from '../conditional-imports/tsutils'; + import { getTypeOfPropertyOfType, nullThrows, NullThrowsReasons } from '.'; const enum Readonlyness { @@ -73,6 +71,9 @@ function isTypeReadonlyObject( type: ts.Type, seenTypes: Set, ): Readonlyness { + assert(!(conditionalTypeScript instanceof Error)); + assert(!(conditionalTsutils instanceof Error)); + function checkIndexSignature(kind: ts.IndexKind): Readonlyness { const indexInfo = checker.getIndexInfoOfType(type, kind); if (indexInfo) { @@ -88,7 +89,13 @@ function isTypeReadonlyObject( if (properties.length) { // ensure the properties are marked as readonly for (const property of properties) { - if (!isPropertyReadonlyInType(type, property.getEscapedName(), checker)) { + if ( + !conditionalTsutils.isPropertyReadonlyInType( + type, + property.getEscapedName(), + checker, + ) + ) { return Readonlyness.Mutable; } } @@ -120,12 +127,16 @@ function isTypeReadonlyObject( } } - const isStringIndexSigReadonly = checkIndexSignature(ts.IndexKind.String); + const isStringIndexSigReadonly = checkIndexSignature( + conditionalTypeScript.IndexKind.String, + ); if (isStringIndexSigReadonly === Readonlyness.Mutable) { return isStringIndexSigReadonly; } - const isNumberIndexSigReadonly = checkIndexSignature(ts.IndexKind.Number); + const isNumberIndexSigReadonly = checkIndexSignature( + conditionalTypeScript.IndexKind.Number, + ); if (isNumberIndexSigReadonly === Readonlyness.Mutable) { return isNumberIndexSigReadonly; } @@ -139,20 +150,26 @@ function isTypeReadonlyRecurser( type: ts.Type, seenTypes: Set, ): Readonlyness.Readonly | Readonlyness.Mutable { + assert(!(conditionalTypeScript instanceof Error)); + assert(!(conditionalTsutils instanceof Error)); + seenTypes.add(type); - if (isUnionType(type)) { + if (conditionalTsutils.isUnionType(type)) { // all types in the union must be readonly - const result = unionTypeParts(type).every(t => - isTypeReadonlyRecurser(checker, t, seenTypes), - ); + const result = conditionalTsutils + .unionTypeParts(type) + .every(t => isTypeReadonlyRecurser(checker, t, seenTypes)); const readonlyness = result ? Readonlyness.Readonly : Readonlyness.Mutable; return readonlyness; } // all non-object, non-intersection types are readonly. // this should only be primitive types - if (!isObjectType(type) && !isUnionOrIntersectionType(type)) { + if ( + !conditionalTsutils.isObjectType(type) && + !conditionalTsutils.isUnionOrIntersectionType(type) + ) { return Readonlyness.Readonly; } @@ -183,6 +200,13 @@ function isTypeReadonlyRecurser( * Checks if the given type is readonly */ function isTypeReadonly(checker: ts.TypeChecker, type: ts.Type): boolean { + if (conditionalTypeScript instanceof Error) { + throw conditionalTypeScript; + } + if (conditionalTsutils instanceof Error) { + throw conditionalTsutils; + } + return ( isTypeReadonlyRecurser(checker, type, new Set()) === Readonlyness.Readonly ); diff --git a/packages/experimental-utils/src/eslint-utils/propertyTypes.ts b/packages/experimental-utils/src/eslint-utils/propertyTypes.ts index 5e2f1054239d..efeaf7beb482 100644 --- a/packages/experimental-utils/src/eslint-utils/propertyTypes.ts +++ b/packages/experimental-utils/src/eslint-utils/propertyTypes.ts @@ -1,4 +1,4 @@ -import * as ts from 'typescript'; +import type * as ts from 'typescript'; export function getTypeOfPropertyOfName( checker: ts.TypeChecker,