diff --git a/.changeset/sharp-tools-cross.md b/.changeset/sharp-tools-cross.md new file mode 100644 index 000000000..2e7b457fb --- /dev/null +++ b/.changeset/sharp-tools-cross.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-import-x": patch +--- + +refactor: migrate core utils diff --git a/package.json b/package.json index dc225cacb..e2e44580b 100644 --- a/package.json +++ b/package.json @@ -30,14 +30,17 @@ "export" ], "scripts": { - "build": "rimraf lib && tsc -p src", - "lint": "yarn lint:es && yarn update:eslint-docs --check", + "build": "tsc -p src", + "clean": "rimraf lib", + "lint": "run-p lint:*", + "lint:docs": "yarn update:eslint-docs --check", "lint:es": "eslint . --cache", + "lint:tsc": "tsc -p tsconfig.base.json --noEmit", "prepare": "patch-package", "release": "changeset publish", "test": "jest", "test-compiled": "yarn build && cross-env TEST_COMPILED=1 jest", - "update:eslint-docs": "yarn build && eslint-doc-generator --rule-doc-title-format prefix-name --rule-doc-section-options false --rule-list-split meta.docs.category --ignore-config stage-0 --config-emoji recommended,☑️", + "update:eslint-docs": "eslint-doc-generator --rule-doc-title-format prefix-name --rule-doc-section-options false --rule-list-split meta.docs.category --ignore-config stage-0 --config-emoji recommended,☑️", "watch": "yarn test --watch" }, "peerDependencies": { @@ -75,6 +78,7 @@ "@types/eslint": "^8.56.5", "@types/jest": "^29.5.12", "@types/json-schema": "^7.0.15", + "@types/node": "^20.11.26", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/typescript-estree": "^5.62.0", @@ -94,6 +98,7 @@ "eslint-plugin-json": "^3.1.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", + "npm-run-all2": "^6.1.2", "prettier": "^3.2.5", "redux": "^5.0.1", "rimraf": "^5.0.5", diff --git a/src/core/importType.js b/src/core/import-type.ts similarity index 64% rename from src/core/importType.js rename to src/core/import-type.ts index b3da97981..676ba5574 100644 --- a/src/core/importType.js +++ b/src/core/import-type.ts @@ -6,9 +6,10 @@ import { import { isCoreModule } from '../utils/is-core-module' import { resolve } from '../utils/resolve' -import { getContextPackagePath } from './packagePath' +import { getContextPackagePath } from './package-path' +import { PluginSettings, RuleContext } from '../types' -function baseModule(name) { +function baseModule(name: string) { if (isScoped(name)) { const [scope, pkg] = name.split('/') return `${scope}/${pkg}` @@ -17,17 +18,21 @@ function baseModule(name) { return pkg } -function isInternalRegexMatch(name, settings) { - const internalScope = settings && settings['import-x/internal-regex'] +function isInternalRegexMatch(name: string, settings: PluginSettings) { + const internalScope = settings?.['import-x/internal-regex'] return internalScope && new RegExp(internalScope).test(name) } -export function isAbsolute(name) { +export function isAbsolute(name?: string | boolean | number | null) { return typeof name === 'string' && nodeIsAbsolute(name) } // path is defined only when a resolver resolves to a non-standard path -export function isBuiltIn(name, settings, path) { +export function isBuiltIn( + name: string, + settings: PluginSettings, + path?: string | null, +) { if (path || !name) { return false } @@ -36,7 +41,11 @@ export function isBuiltIn(name, settings, path) { return isCoreModule(base) || extras.indexOf(base) > -1 } -export function isExternalModule(name, path, context) { +export function isExternalModule( + name: string, + path: string, + context: RuleContext, +) { if (arguments.length < 3) { throw new TypeError( 'isExternalModule: name, path, and context are all required', @@ -48,7 +57,11 @@ export function isExternalModule(name, path, context) { ) } -export function isExternalModuleMain(name, path, context) { +export function isExternalModuleMain( + name: string, + path: string, + context: RuleContext, +) { if (arguments.length < 3) { throw new TypeError( 'isExternalModule: name, path, and context are all required', @@ -58,39 +71,44 @@ export function isExternalModuleMain(name, path, context) { } const moduleRegExp = /^\w/ -function isModule(name) { + +function isModule(name: string) { return name && moduleRegExp.test(name) } const moduleMainRegExp = /^[\w]((?!\/).)*$/ -function isModuleMain(name) { + +function isModuleMain(name: string) { return name && moduleMainRegExp.test(name) } const scopedRegExp = /^@[^/]+\/?[^/]+/ -export function isScoped(name) { + +export function isScoped(name: string) { return name && scopedRegExp.test(name) } const scopedMainRegExp = /^@[^/]+\/?[^/]+$/ -export function isScopedMain(name) { + +export function isScopedMain(name: string) { return name && scopedMainRegExp.test(name) } -function isRelativeToParent(name) { +function isRelativeToParent(name: string) { return /^\.\.$|^\.\.[\\/]/.test(name) } const indexFiles = ['.', './', './index', './index.js'] -function isIndex(name) { - return indexFiles.indexOf(name) !== -1 + +function isIndex(name: string) { + return indexFiles.includes(name) } -function isRelativeToSibling(name) { +function isRelativeToSibling(name: string) { return /^\.[\\/]/.test(name) } -function isExternalPath(path, context) { +function isExternalPath(path: string | null | undefined, context: RuleContext) { if (!path) { return false } @@ -102,8 +120,9 @@ function isExternalPath(path, context) { return true } - const folders = (settings && - settings['import-x/external-module-folders']) || ['node_modules'] + const folders = settings?.['import-x/external-module-folders'] || [ + 'node_modules', + ] return folders.some(folder => { const folderPath = nodeResolve(packagePath, folder) const relativePath = relative(folderPath, path) @@ -111,7 +130,7 @@ function isExternalPath(path, context) { }) } -function isInternalPath(path, context) { +function isInternalPath(path: string | null | undefined, context: RuleContext) { if (!path) { return false } @@ -119,28 +138,28 @@ function isInternalPath(path, context) { return !relative(packagePath, path).startsWith('../') } -function isExternalLookingName(name) { +function isExternalLookingName(name: string) { return isModule(name) || isScoped(name) } -function typeTest(name, context, path) { +function typeTest(name: string, context: RuleContext, path?: string | null) { const { settings } = context if (isInternalRegexMatch(name, settings)) { return 'internal' } - if (isAbsolute(name, settings, path)) { + if (isAbsolute(name)) { return 'absolute' } if (isBuiltIn(name, settings, path)) { return 'builtin' } - if (isRelativeToParent(name, settings, path)) { + if (isRelativeToParent(name)) { return 'parent' } - if (isIndex(name, settings, path)) { + if (isIndex(name)) { return 'index' } - if (isRelativeToSibling(name, settings, path)) { + if (isRelativeToSibling(name)) { return 'sibling' } if (isExternalPath(path, context)) { @@ -155,6 +174,6 @@ function typeTest(name, context, path) { return 'unknown' } -export default function resolveImportType(name, context) { +export function importType(name: string, context: RuleContext) { return typeTest(name, context, resolve(name, context)) } diff --git a/src/core/package-path.ts b/src/core/package-path.ts new file mode 100644 index 000000000..d7aca9dfc --- /dev/null +++ b/src/core/package-path.ts @@ -0,0 +1,27 @@ +import { dirname } from 'path' + +import { pkgUp } from '../utils/pkg-up' +import { readPkgUp } from '../utils/read-pkg-ip' +import { RuleContext } from '../types' + +export function getContextPackagePath(context: RuleContext) { + return getFilePackagePath( + context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename(), + ) +} + +export function getFilePackagePath(filePath: string) { + const fp = pkgUp({ cwd: filePath })! + return dirname(fp) +} + +export function getFilePackageName(filePath: string): string | null { + const { pkg, path } = readPkgUp({ cwd: filePath }) + if (pkg) { + // recursion in case of intermediate esm package.json without name found + return pkg.name || getFilePackageName(dirname(dirname(path))) + } + return null +} diff --git a/src/core/packagePath.js b/src/core/packagePath.js deleted file mode 100644 index d0a360edc..000000000 --- a/src/core/packagePath.js +++ /dev/null @@ -1,25 +0,0 @@ -import { dirname } from 'path' -import { pkgUp } from '../utils/pkgUp' -import { readPkgUp } from '../utils/readPkgUp' - -export function getContextPackagePath(context) { - return getFilePackagePath( - context.getPhysicalFilename - ? context.getPhysicalFilename() - : context.getFilename(), - ) -} - -export function getFilePackagePath(filePath) { - const fp = pkgUp({ cwd: filePath }) - return dirname(fp) -} - -export function getFilePackageName(filePath) { - const { pkg, path } = readPkgUp({ cwd: filePath, normalize: false }) - if (pkg) { - // recursion in case of intermediate esm package.json without name found - return pkg.name || getFilePackageName(dirname(dirname(path))) - } - return null -} diff --git a/src/core/staticRequire.js b/src/core/static-require.ts similarity index 70% rename from src/core/staticRequire.js rename to src/core/static-require.ts index cd6dcd528..cbb4e4843 100644 --- a/src/core/staticRequire.js +++ b/src/core/static-require.ts @@ -1,5 +1,7 @@ +import type { TSESTree } from '@typescript-eslint/utils' + // todo: merge with module visitor -export default function isStaticRequire(node) { +export function isStaticRequire(node: TSESTree.CallExpression) { return ( node && node.callee && diff --git a/src/ExportMap.ts b/src/export-map.ts similarity index 99% rename from src/ExportMap.ts rename to src/export-map.ts index 89651848d..3899e502c 100644 --- a/src/ExportMap.ts +++ b/src/export-map.ts @@ -660,7 +660,7 @@ export class ExportMap { private declare mtime: Date - private declare doc: Annotation + declare doc: Annotation constructor(public path: string) {} @@ -812,8 +812,6 @@ export class ExportMap { } } } - - return undefined } forEach( @@ -1072,7 +1070,7 @@ function childContext( : 'getFilename' in context && typeof context.getFilename === 'function' ? context.getFilename() - : (('filename' in context && context.filename) as string), + : ('filename' in context && context.filename) || undefined, } } diff --git a/src/rules/default.js b/src/rules/default.js index ce7dea0d9..9ffa9ad92 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -1,4 +1,4 @@ -import { ExportMap } from '../ExportMap' +import { ExportMap } from '../export-map' import { docsUrl } from '../docs-url' module.exports = { diff --git a/src/rules/export.js b/src/rules/export.js index ac0892005..7504db501 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -1,4 +1,4 @@ -import { ExportMap, recursivePatternCapture } from '../ExportMap' +import { ExportMap, recursivePatternCapture } from '../export-map' import { docsUrl } from '../docs-url' /* diff --git a/src/rules/extensions.js b/src/rules/extensions.js index 734c1a69a..caf504175 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -1,8 +1,8 @@ import path from 'path' import { resolve } from '../utils/resolve' -import { isBuiltIn, isExternalModule, isScoped } from '../core/importType' -import { moduleVisitor } from '../utils/moduleVisitor' +import { isBuiltIn, isExternalModule, isScoped } from '../core/import-type' +import { moduleVisitor } from '../utils/module-visitor' import { docsUrl } from '../docs-url' const enumValues = { enum: ['always', 'ignorePackages', 'never'] } diff --git a/src/rules/max-dependencies.js b/src/rules/max-dependencies.js index f196e66bf..b8df2580b 100644 --- a/src/rules/max-dependencies.js +++ b/src/rules/max-dependencies.js @@ -1,4 +1,4 @@ -import { moduleVisitor } from '../utils/moduleVisitor' +import { moduleVisitor } from '../utils/module-visitor' import { docsUrl } from '../docs-url' const DEFAULT_MAX = 10 diff --git a/src/rules/named.ts b/src/rules/named.ts index 0f39cc17e..a59b4bd42 100644 --- a/src/rules/named.ts +++ b/src/rules/named.ts @@ -2,9 +2,9 @@ import path from 'path' import type { TSESTree } from '@typescript-eslint/utils' -import { ExportMap } from '../ExportMap' +import { ExportMap } from '../export-map' import { createRule } from '../utils' -import { ModuleOptions } from '../utils/moduleVisitor' +import { ModuleOptions } from '../utils/module-visitor' type MessageId = 'notFound' | 'notFoundDeep' diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 72d5d94c9..8e0e69de7 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -1,5 +1,5 @@ -import declaredScope from '../utils/declaredScope' -import { ExportMap } from '../ExportMap' +import { declaredScope } from '../utils/declared-scope' +import { ExportMap } from '../export-map' import { importDeclaration } from '../import-declaration' import { docsUrl } from '../docs-url' diff --git a/src/rules/newline-after-import.js b/src/rules/newline-after-import.js index 02f6dbd24..b9f8c80e9 100644 --- a/src/rules/newline-after-import.js +++ b/src/rules/newline-after-import.js @@ -3,7 +3,7 @@ * @author Radek Benkel */ -import isStaticRequire from '../core/staticRequire' +import { isStaticRequire } from '../core/static-require' import { docsUrl } from '../docs-url' import debug from 'debug' diff --git a/src/rules/no-absolute-path.js b/src/rules/no-absolute-path.js index d416fbc81..563840032 100644 --- a/src/rules/no-absolute-path.js +++ b/src/rules/no-absolute-path.js @@ -1,6 +1,6 @@ import path from 'path' -import { moduleVisitor, makeOptionsSchema } from '../utils/moduleVisitor' -import { isAbsolute } from '../core/importType' +import { moduleVisitor, makeOptionsSchema } from '../utils/module-visitor' +import { isAbsolute } from '../core/import-type' import { docsUrl } from '../docs-url' module.exports = { diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js index b8f7b1656..ca50e1dd7 100644 --- a/src/rules/no-cycle.js +++ b/src/rules/no-cycle.js @@ -3,9 +3,9 @@ */ import { resolve } from '../utils/resolve' -import { ExportMap } from '../ExportMap' -import { isExternalModule } from '../core/importType' -import { moduleVisitor, makeOptionsSchema } from '../utils/moduleVisitor' +import { ExportMap } from '../export-map' +import { isExternalModule } from '../core/import-type' +import { moduleVisitor, makeOptionsSchema } from '../utils/module-visitor' import { docsUrl } from '../docs-url' const traversed = new Set() diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index d1e91edfa..7cab95609 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -1,5 +1,5 @@ -import declaredScope from '../utils/declaredScope' -import { ExportMap } from '../ExportMap' +import { declaredScope } from '../utils/declared-scope' +import { ExportMap } from '../export-map' import { docsUrl } from '../docs-url' function message(deprecation) { diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.js index 467f1af40..a14c8abbd 100644 --- a/src/rules/no-extraneous-dependencies.js +++ b/src/rules/no-extraneous-dependencies.js @@ -1,11 +1,11 @@ import path from 'path' import fs from 'fs' -import { pkgUp } from '../utils/pkgUp' +import { pkgUp } from '../utils/pkg-up' import { minimatch } from 'minimatch' import { resolve } from '../utils/resolve' -import { moduleVisitor } from '../utils/moduleVisitor' -import importType from '../core/importType' -import { getFilePackageName } from '../core/packagePath' +import { moduleVisitor } from '../utils/module-visitor' +import { importType } from '../core/import-type' +import { getFilePackageName } from '../core/package-path' import { docsUrl } from '../docs-url' const depFieldCache = new Map() diff --git a/src/rules/no-import-module-exports.js b/src/rules/no-import-module-exports.js index 5a2995952..bb464c0ea 100644 --- a/src/rules/no-import-module-exports.js +++ b/src/rules/no-import-module-exports.js @@ -1,6 +1,6 @@ import { minimatch } from 'minimatch' import path from 'path' -import { pkgUp } from '../utils/pkgUp' +import { pkgUp } from '../utils/pkg-up' function getEntryPoint(context) { const pkgPath = pkgUp({ diff --git a/src/rules/no-internal-modules.js b/src/rules/no-internal-modules.js index e8f55eaaa..f02c2d09b 100644 --- a/src/rules/no-internal-modules.js +++ b/src/rules/no-internal-modules.js @@ -1,8 +1,8 @@ import { makeRe } from 'minimatch' import { resolve } from '../utils/resolve' -import importType from '../core/importType' -import { moduleVisitor } from '../utils/moduleVisitor' +import { importType } from '../core/import-type' +import { moduleVisitor } from '../utils/module-visitor' import { docsUrl } from '../docs-url' module.exports = { diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js index 8d36e7281..325a36f6b 100644 --- a/src/rules/no-named-as-default-member.js +++ b/src/rules/no-named-as-default-member.js @@ -1,7 +1,7 @@ /** * Rule to warn about potentially confused use of name exports */ -import { ExportMap } from '../ExportMap' +import { ExportMap } from '../export-map' import { importDeclaration } from '../import-declaration' import { docsUrl } from '../docs-url' diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js index 31b023c2b..dfd6ebc03 100644 --- a/src/rules/no-named-as-default.js +++ b/src/rules/no-named-as-default.js @@ -1,4 +1,4 @@ -import { ExportMap } from '../ExportMap' +import { ExportMap } from '../export-map' import { importDeclaration } from '../import-declaration' import { docsUrl } from '../docs-url' diff --git a/src/rules/no-nodejs-modules.js b/src/rules/no-nodejs-modules.js index 4c3303947..bd5f6d5f9 100644 --- a/src/rules/no-nodejs-modules.js +++ b/src/rules/no-nodejs-modules.js @@ -1,5 +1,5 @@ -import importType from '../core/importType' -import { moduleVisitor } from '../utils/moduleVisitor' +import { importType } from '../core/import-type' +import { moduleVisitor } from '../utils/module-visitor' import { docsUrl } from '../docs-url' function reportIfMissing(context, node, allowed, name) { diff --git a/src/rules/no-relative-packages.js b/src/rules/no-relative-packages.js index b1f5bd7a8..09f76149b 100644 --- a/src/rules/no-relative-packages.js +++ b/src/rules/no-relative-packages.js @@ -1,9 +1,9 @@ import path from 'path' -import { readPkgUp } from '../utils/readPkgUp' +import { readPkgUp } from '../utils/read-pkg-ip' import { resolve } from '../utils/resolve' -import { moduleVisitor, makeOptionsSchema } from '../utils/moduleVisitor' -import importType from '../core/importType' +import { moduleVisitor, makeOptionsSchema } from '../utils/module-visitor' +import { importType } from '../core/import-type' import { docsUrl } from '../docs-url' /** @param {string} filePath */ diff --git a/src/rules/no-relative-parent-imports.js b/src/rules/no-relative-parent-imports.js index 7b52c7a12..ac8fa3f2d 100644 --- a/src/rules/no-relative-parent-imports.js +++ b/src/rules/no-relative-parent-imports.js @@ -1,9 +1,9 @@ -import { moduleVisitor, makeOptionsSchema } from '../utils/moduleVisitor' +import { moduleVisitor, makeOptionsSchema } from '../utils/module-visitor' import { docsUrl } from '../docs-url' import { basename, dirname, relative } from 'path' import { resolve } from '../utils/resolve' -import importType from '../core/importType' +import { importType } from '../core/import-type' module.exports = { meta: { diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index 84b914ea4..f7377e8f2 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -1,11 +1,11 @@ import path from 'path' import { resolve } from '../utils/resolve' -import { moduleVisitor } from '../utils/moduleVisitor' +import { moduleVisitor } from '../utils/module-visitor' import isGlob from 'is-glob' import { Minimatch } from 'minimatch' import { docsUrl } from '../docs-url' -import importType from '../core/importType' +import { importType } from '../core/import-type' const containsPath = (filepath, target) => { const relative = path.relative(target, filepath) diff --git a/src/rules/no-self-import.js b/src/rules/no-self-import.js index 4f43ecf32..e354e09e5 100644 --- a/src/rules/no-self-import.js +++ b/src/rules/no-self-import.js @@ -4,7 +4,7 @@ */ import { resolve } from '../utils/resolve' -import { moduleVisitor } from '../utils/moduleVisitor' +import { moduleVisitor } from '../utils/module-visitor' import { docsUrl } from '../docs-url' function isImportingSelf(context, node, requireName) { diff --git a/src/rules/no-unassigned-import.js b/src/rules/no-unassigned-import.js index 0f5423125..cc81f2f00 100644 --- a/src/rules/no-unassigned-import.js +++ b/src/rules/no-unassigned-import.js @@ -1,7 +1,7 @@ import path from 'path' import { minimatch } from 'minimatch' -import isStaticRequire from '../core/staticRequire' +import { isStaticRequire } from '../core/static-require' import { docsUrl } from '../docs-url' function report(context, node) { diff --git a/src/rules/no-unresolved.ts b/src/rules/no-unresolved.ts index 6f8fe0a96..7d3db0c26 100644 --- a/src/rules/no-unresolved.ts +++ b/src/rules/no-unresolved.ts @@ -7,12 +7,12 @@ import { fileExistsWithCaseSync, resolve, } from '../utils/resolve' -import { ModuleCache } from '../utils/ModuleCache' +import { ModuleCache } from '../utils/module-cache' import { ModuleOptions, makeOptionsSchema, moduleVisitor, -} from '../utils/moduleVisitor' +} from '../utils/module-visitor' import { createRule } from '../utils' type Options = [ diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index 7ed436596..22b992085 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -7,9 +7,9 @@ import { getFileExtensions } from '../utils/ignore' import { resolve } from '../utils/resolve' import { visit } from '../utils/visit' import { dirname, join } from 'path' -import { readPkgUp } from '../utils/readPkgUp' +import { readPkgUp } from '../utils/read-pkg-ip' -import { ExportMap, recursivePatternCapture } from '../ExportMap' +import { ExportMap, recursivePatternCapture } from '../export-map' import { docsUrl } from '../docs-url' let FileEnumerator diff --git a/src/rules/no-useless-path-segments.js b/src/rules/no-useless-path-segments.js index 35a854a77..3ba7098e3 100644 --- a/src/rules/no-useless-path-segments.js +++ b/src/rules/no-useless-path-segments.js @@ -4,7 +4,7 @@ */ import { getFileExtensions } from '../utils/ignore' -import { moduleVisitor } from '../utils/moduleVisitor' +import { moduleVisitor } from '../utils/module-visitor' import { resolve } from '../utils/resolve' import path from 'path' import { docsUrl } from '../docs-url' diff --git a/src/rules/no-webpack-loader-syntax.js b/src/rules/no-webpack-loader-syntax.js index a7f8f60d2..8891490a7 100644 --- a/src/rules/no-webpack-loader-syntax.js +++ b/src/rules/no-webpack-loader-syntax.js @@ -1,4 +1,4 @@ -import { moduleVisitor } from '../utils/moduleVisitor' +import { moduleVisitor } from '../utils/module-visitor' import { docsUrl } from '../docs-url' function reportIfNonStandard(context, node, name) { diff --git a/src/rules/order.js b/src/rules/order.js index 87ebc6bd6..eee46d7af 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -2,8 +2,8 @@ import { minimatch } from 'minimatch' -import importType from '../core/importType' -import isStaticRequire from '../core/staticRequire' +import { importType } from '../core/import-type' +import { isStaticRequire } from '../core/static-require' import { docsUrl } from '../docs-url' // This is a **non-spec compliant** but works in practice replacement of `object.groupby` package. diff --git a/src/types.ts b/src/types.ts index ad5f4e6b3..fc4bd9e8a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,7 +12,7 @@ export interface NodeResolverOptions { } export interface WebpackResolverOptions { - config?: string | ResolveOptions + config?: string | { resolve: Omit } 'config-index'?: number env?: Record argv?: Record @@ -22,24 +22,29 @@ export type FileExtension = `.${string}` export type DocStyle = 'jsdoc' | 'tomdoc' +export type Arrayable = T | readonly T[] + export interface ImportSettings { cache?: { - lifetime: number | '∞' | 'Infinity' + lifetime?: number | '∞' | 'Infinity' } coreModules?: string[] docstyle?: DocStyle[] extensions?: readonly FileExtension[] externalModuleFolders?: string[] ignore?: string[] - parsers?: Record + internalRegex?: string + parsers?: Record resolve?: NodeResolverOptions - resolver?: + resolver?: Arrayable< | LiteralUnion<'node' | 'typescript' | 'webpack', string> | { node?: boolean | NodeResolverOptions typescript?: boolean | TsResolverOptions webpack?: WebpackResolverOptions + [resolve: string]: unknown } + > } export type WithPluginName = T extends string @@ -61,8 +66,8 @@ export interface RuleContext< TOptions extends readonly unknown[] = readonly unknown[], > extends Omit, 'settings'> { languageOptions?: { - parser: TSESLint.Linter.ParserModule - parserOptions: TSESLint.ParserOptions + parser?: TSESLint.Linter.ParserModule + parserOptions?: TSESLint.ParserOptions } settings: PluginSettings } @@ -70,10 +75,10 @@ export interface RuleContext< export interface ChildContext { cacheKey: string settings: PluginSettings - parserPath: string - parserOptions: TSESLint.ParserOptions + parserPath?: string | null + parserOptions?: TSESLint.ParserOptions path: string - filename: string + filename?: string } export interface ParseError extends Error { diff --git a/src/utils/declaredScope.js b/src/utils/declared-scope.ts similarity index 61% rename from src/utils/declaredScope.js rename to src/utils/declared-scope.ts index 56e689394..1ff8593c9 100644 --- a/src/utils/declaredScope.js +++ b/src/utils/declared-scope.ts @@ -1,9 +1,6 @@ -'use strict' +import type { RuleContext } from '../types' -exports.__esModule = true - -/** @type {import('./declaredScope').default} */ -exports.default = function declaredScope(context, name) { +export function declaredScope(context: RuleContext, name: string) { const references = context.getScope().references const reference = references.find(x => x.identifier.name === name) if (!reference || !reference.resolved) { diff --git a/src/utils/declaredScope.d.ts b/src/utils/declaredScope.d.ts deleted file mode 100644 index 1ff5aea3a..000000000 --- a/src/utils/declaredScope.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Rule, Scope } from 'eslint' - -declare function declaredScope( - context: Rule.RuleContext, - name: string, -): Scope.Scope['type'] | undefined - -export default declaredScope diff --git a/src/utils/ignore.ts b/src/utils/ignore.ts index 876e5f319..eac3df10e 100644 --- a/src/utils/ignore.ts +++ b/src/utils/ignore.ts @@ -2,7 +2,12 @@ import { extname } from 'path' import debug from 'debug' -import type { ChildContext, FileExtension, PluginSettings } from '../types' +import type { + ChildContext, + FileExtension, + PluginSettings, + RuleContext, +} from '../types' const log = debug('eslint-plugin-import-x:utils:ignore') @@ -10,7 +15,7 @@ const log = debug('eslint-plugin-import-x:utils:ignore') let cachedSet: Set let lastSettings: PluginSettings -function validExtensions(context: ChildContext) { +function validExtensions(context: ChildContext | RuleContext) { if (cachedSet && context.settings === lastSettings) { return cachedSet } @@ -40,7 +45,7 @@ export function getFileExtensions(settings: PluginSettings) { return exts } -export function ignore(path: string, context: ChildContext) { +export function ignore(path: string, context: ChildContext | RuleContext) { // check extension whitelist first (cheap) if (!hasValidExtension(path, context)) { return true @@ -65,7 +70,7 @@ export function ignore(path: string, context: ChildContext) { export function hasValidExtension( path: string, - context: ChildContext, + context: ChildContext | RuleContext, ): path is `${string}${FileExtension}` { return validExtensions(context).has(extname(path) as FileExtension) } diff --git a/src/utils/index.ts b/src/utils/index.ts index b163699ef..9439ddf37 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './constants' export * from './create-rule' +export * from './module-require' diff --git a/src/utils/ModuleCache.ts b/src/utils/module-cache.ts similarity index 89% rename from src/utils/ModuleCache.ts rename to src/utils/module-cache.ts index 8d8819dff..a5b4245f5 100644 --- a/src/utils/ModuleCache.ts +++ b/src/utils/module-cache.ts @@ -1,6 +1,8 @@ -import { ImportSettings, PluginSettings } from '../types' +import debug from 'debug' -const log = require('debug')('eslint-plugin-import-x:utils:ModuleCache') +import type { ImportSettings, PluginSettings } from '../types' + +const log = debug('eslint-plugin-import-x:utils:ModuleCache') export type CacheKey = unknown diff --git a/src/utils/module-require.d.ts b/src/utils/module-require.d.ts deleted file mode 100644 index a0b74929d..000000000 --- a/src/utils/module-require.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare function moduleRequire(p: string): T - -export default moduleRequire diff --git a/src/utils/moduleVisitor.ts b/src/utils/module-visitor.ts similarity index 100% rename from src/utils/moduleVisitor.ts rename to src/utils/module-visitor.ts diff --git a/src/utils/pkgDir.ts b/src/utils/pkg-dir.ts similarity index 80% rename from src/utils/pkgDir.ts rename to src/utils/pkg-dir.ts index 471cc488d..e9f4002db 100644 --- a/src/utils/pkgDir.ts +++ b/src/utils/pkg-dir.ts @@ -1,6 +1,6 @@ import path from 'path' -import { pkgUp } from './pkgUp' +import { pkgUp } from './pkg-up' export function pkgDir(cwd: string) { const fp = pkgUp({ cwd }) diff --git a/src/utils/pkgUp.ts b/src/utils/pkg-up.ts similarity index 100% rename from src/utils/pkgUp.ts rename to src/utils/pkg-up.ts diff --git a/src/utils/readPkgUp.ts b/src/utils/read-pkg-ip.ts similarity index 93% rename from src/utils/readPkgUp.ts rename to src/utils/read-pkg-ip.ts index 69e62fb96..ec3fb575e 100644 --- a/src/utils/readPkgUp.ts +++ b/src/utils/read-pkg-ip.ts @@ -2,7 +2,7 @@ import fs from 'fs' import type { PackageJson } from 'type-fest' -import { pkgUp } from './pkgUp' +import { pkgUp } from './pkg-up' function stripBOM(str: string) { return str.replace(/^\uFEFF/, '') diff --git a/src/utils/resolve.ts b/src/utils/resolve.ts index 1115e898d..b74bae02c 100644 --- a/src/utils/resolve.ts +++ b/src/utils/resolve.ts @@ -4,12 +4,17 @@ import fs from 'fs' import Module from 'module' import path from 'path' -import { ImportSettings, PluginSettings, RuleContext } from '../types' +import type { + Arrayable, + ImportSettings, + PluginSettings, + RuleContext, +} from '../types' import { hashObject } from './hash' -import { ModuleCache } from './ModuleCache' +import { ModuleCache } from './module-cache' -import { pkgDir } from './pkgDir' +import { pkgDir } from './pkg-dir' export interface ResultNotFound { found: false @@ -79,7 +84,7 @@ function tryRequire( // https://stackoverflow.com/a/27382838 export function fileExistsWithCaseSync( filepath: string | null, - cacheSettings: ImportSettings['cache'], + cacheSettings?: ImportSettings['cache'], strict?: boolean, ): boolean { // don't care if the FS is case-sensitive @@ -212,7 +217,7 @@ export function relative( } function resolverReducer( - resolvers: string[] | string | Record, + resolvers: Arrayable>, map: Map, ) { if (Array.isArray(resolvers)) { @@ -226,8 +231,8 @@ function resolverReducer( } if (typeof resolvers === 'object') { - for (const key in resolvers) { - map.set(key, resolvers[key]) + for (const [key, value] of Object.entries(resolvers)) { + map.set(key, value) } return map } diff --git a/test/cli.spec.js b/test/cli.spec.js deleted file mode 100644 index 3b8f246d6..000000000 --- a/test/cli.spec.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * tests that require fully booting up ESLint - */ -import path from 'path' - -import { CLIEngine, ESLint } from 'eslint' -import eslintPkg from 'eslint/package.json' -import semver from 'semver' -import importPlugin from '../src/index' - -describe('CLI regression tests', () => { - describe('issue #210', () => { - let eslint - let cli - beforeAll(() => { - if (ESLint) { - eslint = new ESLint({ - useEslintrc: false, - overrideConfigFile: './test/fixtures/issue210.config.js', - rulePaths: ['./src/rules'], - overrideConfig: { - rules: { - named: 2, - }, - }, - plugins: { 'eslint-plugin-import-x': importPlugin }, - }) - } else { - cli = new CLIEngine({ - useEslintrc: false, - configFile: './test/fixtures/issue210.config.js', - rulePaths: ['./src/rules'], - rules: { - named: 2, - }, - }) - cli.addPlugin('eslint-plugin-import-x', importPlugin) - } - }) - - it("doesn't throw an error on gratuitous, erroneous self-reference", () => { - if (eslint) { - return eslint - .lintFiles(['./test/fixtures/issue210.js']) - .catch(() => expect.fail()) - } else { - expect(() => - cli.executeOnFiles(['./test/fixtures/issue210.js']), - ).not.toThrow() - } - }) - }) - - describe('issue #1645', () => { - let eslint - let cli - beforeEach(() => { - if (semver.satisfies(eslintPkg.version, '< 6')) { - this.skip() - } else { - if (ESLint) { - eslint = new ESLint({ - useEslintrc: false, - overrideConfigFile: - './test/fixtures/just-json-files/.eslintrc.json', - rulePaths: ['./src/rules'], - ignore: false, - plugins: { - 'eslint-plugin-import-x': importPlugin, - }, - }) - } else { - cli = new CLIEngine({ - useEslintrc: false, - configFile: './test/fixtures/just-json-files/.eslintrc.json', - rulePaths: ['./src/rules'], - ignore: false, - }) - cli.addPlugin('eslint-plugin-import-x', importPlugin) - } - } - }) - - it('throws an error on invalid JSON', () => { - const invalidJSON = './test/fixtures/just-json-files/invalid.json' - if (eslint) { - return eslint.lintFiles([invalidJSON]).then(results => { - expect(results).toEqual([ - { - filePath: path.resolve(invalidJSON), - messages: [ - { - column: 2, - endColumn: 3, - endLine: 1, - line: 1, - message: 'Expected a JSON object, array or literal.', - nodeType: results[0].messages[0].nodeType, // we don't care about this one - ruleId: 'json/*', - severity: 2, - source: results[0].messages[0].source, // NewLine-characters might differ depending on git-settings - }, - ], - errorCount: 1, - ...(semver.satisfies(eslintPkg.version, '>= 7.32 || ^8.0.0') && { - fatalErrorCount: 0, - }), - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0, - source: results[0].source, // NewLine-characters might differ depending on git-settings - ...(semver.satisfies(eslintPkg.version, '>= 8.8') && { - suppressedMessages: [], - }), - usedDeprecatedRules: results[0].usedDeprecatedRules, // we don't care about this one - }, - ]) - }) - } else { - const results = cli.executeOnFiles([invalidJSON]) - expect(results).toEqual({ - results: [ - { - filePath: path.resolve(invalidJSON), - messages: [ - { - column: 2, - endColumn: 3, - endLine: 1, - line: 1, - message: 'Expected a JSON object, array or literal.', - nodeType: results.results[0].messages[0].nodeType, // we don't care about this one - ruleId: 'json/*', - severity: 2, - source: results.results[0].messages[0].source, // NewLine-characters might differ depending on git-settings - }, - ], - errorCount: 1, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0, - source: results.results[0].source, // NewLine-characters might differ depending on git-settings - }, - ], - errorCount: 1, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0, - usedDeprecatedRules: results.usedDeprecatedRules, // we don't care about this one - }) - } - }) - }) -}) diff --git a/test/cli.spec.ts b/test/cli.spec.ts new file mode 100644 index 000000000..bc1371334 --- /dev/null +++ b/test/cli.spec.ts @@ -0,0 +1,76 @@ +/** + * tests that require fully booting up ESLint + */ +import path from 'path' + +import { ESLint } from 'eslint' +import eslintPkg from 'eslint/package.json' +import semver from 'semver' +import * as importPlugin from 'eslint-plugin-import-x' + +describe('CLI regression tests', () => { + describe('issue #210', () => { + it("doesn't throw an error on gratuitous, erroneous self-reference", () => { + const eslint = new ESLint({ + useEslintrc: false, + overrideConfigFile: './test/fixtures/issue210.config.js', + rulePaths: ['./src/rules'], + overrideConfig: { + rules: { + named: 2, + }, + }, + plugins: { + 'eslint-plugin-import-x': importPlugin as unknown as ESLint.Plugin, + }, + }) + return eslint.lintFiles(['./test/fixtures/issue210.js']) + }) + }) + + describe('issue #1645', () => { + it('throws an error on invalid JSON', async () => { + const invalidJSON = './test/fixtures/just-json-files/invalid.json' + const eslint = new ESLint({ + useEslintrc: false, + overrideConfigFile: './test/fixtures/just-json-files/.eslintrc.json', + rulePaths: ['./src/rules'], + ignore: false, + plugins: { + 'eslint-plugin-import-x': importPlugin as unknown as ESLint.Plugin, + }, + }) + const results = await eslint.lintFiles([invalidJSON]) + expect(results).toEqual([ + { + filePath: path.resolve(invalidJSON), + messages: [ + { + column: 2, + endColumn: 3, + endLine: 1, + line: 1, + message: 'Expected a JSON object, array or literal.', + nodeType: results[0].messages[0].nodeType, // we don't care about this one + ruleId: 'json/*', + severity: 2, + source: results[0].messages[0].source, // NewLine-characters might differ depending on git-settings + }, + ], + errorCount: 1, + ...(semver.satisfies(eslintPkg.version, '>= 7.32 || ^8.0.0') && { + fatalErrorCount: 0, + }), + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: results[0].source, // NewLine-characters might differ depending on git-settings + ...(semver.satisfies(eslintPkg.version, '>= 8.8') && { + suppressedMessages: [], + }), + usedDeprecatedRules: results[0].usedDeprecatedRules, // we don't care about this one + }, + ]) + }) + }) +}) diff --git a/test/config/typescript.spec.js b/test/config/typescript.spec.ts similarity index 86% rename from test/config/typescript.spec.js rename to test/config/typescript.spec.ts index b59547c9b..642d71be8 100644 --- a/test/config/typescript.spec.js +++ b/test/config/typescript.spec.ts @@ -1,4 +1,4 @@ -const config = require('eslint-plugin-import-x/config/typescript') +import config from '../../src/config/typescript' describe('config typescript', () => { // https://github.com/import-js/eslint-plugin-import/issues/1525 diff --git a/test/core/eslintParser.js b/test/core/eslint-parser.ts similarity index 76% rename from test/core/eslintParser.js rename to test/core/eslint-parser.ts index 6ff96f39d..bb6106a42 100644 --- a/test/core/eslintParser.js +++ b/test/core/eslint-parser.ts @@ -1,4 +1,4 @@ -module.exports = { +export = { parseForESLint() { return { ast: {}, diff --git a/test/core/getExports.spec.js b/test/core/export-map.spec.ts similarity index 57% rename from test/core/getExports.spec.js rename to test/core/export-map.spec.ts index 03ceb286a..0f1f8cab7 100644 --- a/test/core/getExports.spec.js +++ b/test/core/export-map.spec.ts @@ -1,40 +1,33 @@ +import fs from 'fs' + import semver from 'semver' import eslintPkg from 'eslint/package.json' import getTsconfig from 'get-tsconfig' +import { setTimeout } from 'timers/promises' -import { ExportMap } from '../../src/ExportMap' - -import fs from 'fs' - -import { getFilename } from '../utils' +import { ExportMap } from '../../src/export-map' +import { testFilePath } from '../utils' import { isMaybeUnambiguousModule } from '../../src/utils/unambiguous' +import type { ChildContext, RuleContext } from '../../src/types' describe('ExportMap', () => { - const fakeContext = Object.assign( - semver.satisfies(eslintPkg.version, '>= 7.28') + const fakeContext = { + ...(semver.satisfies(eslintPkg.version, '>= 7.28') ? { getFilename() { throw new Error( 'Should call getPhysicalFilename() instead of getFilename()', ) }, - getPhysicalFilename: getFilename, + getPhysicalFilename: testFilePath, } - : { - getFilename, - }, - { - settings: {}, - parserPath: '@babel/eslint-parser', - }, - ) + : { getFilename: testFilePath }), + settings: {}, + parserPath: '@babel/eslint-parser', + } as RuleContext it('handles ExportAllDeclaration', () => { - let imports - expect(function () { - imports = ExportMap.get('./export-all', fakeContext) - }).not.toThrow() - + const imports = ExportMap.get('./export-all', fakeContext)! expect(imports).toBeDefined() expect(imports.has('foo')).toBe(true) }) @@ -51,7 +44,7 @@ describe('ExportMap', () => { // mutate (update modified time) const newDate = new Date() - fs.utimes(getFilename('mutator.js'), newDate, newDate, error => { + fs.utimes(testFilePath('mutator.js'), newDate, newDate, error => { expect(error).toBeFalsy() expect(ExportMap.get('./mutator', fakeContext)).not.toBe(firstAccess) done() @@ -82,22 +75,18 @@ describe('ExportMap', () => { }) it('exports explicit names for a missing file in exports', () => { - let imports - expect(function () { - imports = ExportMap.get('./exports-missing', fakeContext) - }).not.toThrow() - + const imports = ExportMap.get('./exports-missing', fakeContext)! expect(imports).toBeDefined() expect(imports.has('bar')).toBe(true) }) it('finds exports for an ES7 module with @babel/eslint-parser', () => { - const path = getFilename('jsx/FooES7.js') + const path = testFilePath('jsx/FooES7.js') const contents = fs.readFileSync(path, { encoding: 'utf8' }) const imports = ExportMap.parse(path, contents, { parserPath: '@babel/eslint-parser', settings: {}, - }) + } as ChildContext)! // imports expect(imports).toBeDefined() @@ -108,19 +97,17 @@ describe('ExportMap', () => { }) describe('deprecation metadata', () => { - function jsdocTests(parseContext, lineEnding) { + function jsdocTests(parseContext: ChildContext, lineEnding: string) { describe('deprecated imports', () => { - let imports - beforeAll(() => { - const path = getFilename('deprecated.js') - const contents = fs - .readFileSync(path, { encoding: 'utf8' }) - .replace(/[\r]\n/g, lineEnding) - imports = ExportMap.parse(path, contents, parseContext) - - // sanity checks - expect(imports.errors).toHaveLength(0) - }) + const path = testFilePath('deprecated.js') + const contents = fs + .readFileSync(path, { encoding: 'utf8' }) + .replace(/\r?\n/g, lineEnding) + + const imports = ExportMap.parse(path, contents, parseContext)! + + // sanity checks + expect(imports.errors).toHaveLength(0) it('works with named imports.', () => { expect(imports.has('fn')).toBe(true) @@ -192,15 +179,12 @@ describe('ExportMap', () => { }) describe('full module', () => { - let imports - beforeAll(() => { - const path = getFilename('deprecated-file.js') - const contents = fs.readFileSync(path, { encoding: 'utf8' }) - imports = ExportMap.parse(path, contents, parseContext) - - // sanity checks - expect(imports.errors).toHaveLength(0) - }) + const path = testFilePath('deprecated-file.js') + const contents = fs.readFileSync(path, { encoding: 'utf8' }) + const imports = ExportMap.parse(path, contents, parseContext)! + + // sanity checks + expect(imports.errors).toHaveLength(0) it('has JSDoc metadata', () => { expect(imports.doc).toBeDefined() @@ -215,10 +199,10 @@ describe('ExportMap', () => { parserOptions: { ecmaVersion: 2015, sourceType: 'module', - attachComment: true, + // attachComment: true, }, settings: {}, - }, + } as ChildContext, '\n', ) jsdocTests( @@ -227,10 +211,10 @@ describe('ExportMap', () => { parserOptions: { ecmaVersion: 2015, sourceType: 'module', - attachComment: true, + // attachComment: true, }, settings: {}, - }, + } as ChildContext, '\r\n', ) }) @@ -242,10 +226,10 @@ describe('ExportMap', () => { parserOptions: { ecmaVersion: 2015, sourceType: 'module', - attachComment: true, + // attachComment: true, }, settings: {}, - }, + } as ChildContext, '\n', ) jsdocTests( @@ -254,10 +238,10 @@ describe('ExportMap', () => { parserOptions: { ecmaVersion: 2015, sourceType: 'module', - attachComment: true, + // attachComment: true, }, settings: {}, - }, + } as ChildContext, '\r\n', ) }) @@ -268,36 +252,49 @@ describe('ExportMap', () => { parserPath: 'espree', parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, settings: {}, - } - // const babelContext = { parserPath: '@babel/eslint-parser', parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, settings: {} }; + } as ChildContext + + const babelContext = { + parserPath: '@babel/eslint-parser', + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + settings: {}, + } as ChildContext it('works with espree & traditional namespace exports', () => { - const path = getFilename('deep/a.js') + const path = testFilePath('deep/a.js') const contents = fs.readFileSync(path, { encoding: 'utf8' }) - const a = ExportMap.parse(path, contents, espreeContext) + const a = ExportMap.parse(path, contents, espreeContext)! expect(a.errors).toHaveLength(0) - expect(a.get('b').namespace).toBeDefined() - expect(a.get('b').namespace.has('c')).toBe(true) + expect(a.get<{ namespace: ExportMap }>('b')!.namespace).toBeDefined() + expect(a.get<{ namespace: ExportMap }>('b')!.namespace.has('c')).toBe( + true, + ) }) it('captures namespace exported as default', () => { - const path = getFilename('deep/default.js') + const path = testFilePath('deep/default.js') const contents = fs.readFileSync(path, { encoding: 'utf8' }) - const def = ExportMap.parse(path, contents, espreeContext) + const def = ExportMap.parse(path, contents, espreeContext)! expect(def.errors).toHaveLength(0) - expect(def.get('default').namespace).toBeDefined() - expect(def.get('default').namespace.has('c')).toBe(true) + expect( + def.get<{ namespace: ExportMap }>('default')!.namespace, + ).toBeDefined() + expect( + def.get<{ namespace: ExportMap }>('default')!.namespace.has('c'), + ).toBe(true) }) // FIXME: check and enable - // it('works with @babel/eslint-parser & ES7 namespace exports', function () { - // const path = getFilename('deep-es7/a.js'); - // const contents = fs.readFileSync(path, { encoding: 'utf8' }); - // const a = ExportMap.parse(path, contents, babelContext); - // expect(a.errors).to.be.empty; - // expect(a.get('b').namespace).to.exist; - // expect(a.get('b').namespace.has('c')).to.be.true; - // }); + it.skip('works with @babel/eslint-parser & ES7 namespace exports', function () { + const path = testFilePath('deep-es7/a.js') + const contents = fs.readFileSync(path, { encoding: 'utf8' }) + const a = ExportMap.parse(path, contents, babelContext)! + expect(a.errors).toHaveLength(0) + expect(a.get<{ namespace: ExportMap }>('b')!.namespace).toBeDefined() + expect(a.get<{ namespace: ExportMap }>('b')!.namespace.has('c')).toBe( + true, + ) + }) }) describe('deep namespace caching', () => { @@ -305,40 +302,47 @@ describe('ExportMap', () => { parserPath: 'espree', parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, settings: {}, - } - let a - beforeAll(done => { - // first version - fs.writeFileSync( - getFilename('deep/cache-2.js'), - fs.readFileSync(getFilename('deep/cache-2a.js')), - ) + } as ChildContext - const path = getFilename('deep/cache-1.js') - const contents = fs.readFileSync(path, { encoding: 'utf8' }) - a = ExportMap.parse(path, contents, espreeContext) - expect(a.errors).toHaveLength(0) + let a: ExportMap | null + + beforeAll(async () => { + try { + // first version + await fs.promises.writeFile( + testFilePath('deep/cache-2.js'), + await fs.promises.readFile(testFilePath('deep/cache-2a.js')), + ) + + const path = testFilePath('deep/cache-1.js') + const contents = await fs.promises.readFile(path, { encoding: 'utf8' }) + a = ExportMap.parse(path, contents, espreeContext)! + expect(a.errors).toHaveLength(0) - expect(a.get('b').namespace).toBeDefined() - expect(a.get('b').namespace.has('c')).toBe(true) + expect(a.get<{ namespace: ExportMap }>('b')!.namespace).toBeDefined() + expect(a.get<{ namespace: ExportMap }>('b')!.namespace.has('c')).toBe( + true, + ) - // wait ~1s, cache check is 1s resolution - setTimeout(function reup() { - fs.unlinkSync(getFilename('deep/cache-2.js')) + // wait ~1s, cache check is 1s resolution + await setTimeout(1100) + } finally { + await fs.promises.unlink(testFilePath('deep/cache-2.js')) // swap in a new file and touch it - fs.writeFileSync( - getFilename('deep/cache-2.js'), - fs.readFileSync(getFilename('deep/cache-2b.js')), + await fs.promises.writeFile( + testFilePath('deep/cache-2.js'), + await fs.promises.readFile(testFilePath('deep/cache-2b.js')), ) - done() - }, 1100) + } }) it('works', () => { - expect(a.get('b').namespace.has('c')).toBe(false) + expect(a!.get<{ namespace: ExportMap }>('b')!.namespace.has('c')).toBe( + false, + ) }) - afterAll(done => fs.unlink(getFilename('deep/cache-2.js'), done)) + afterAll(done => fs.unlink(testFilePath('deep/cache-2.js'), done)) }) describe('Map API', () => { @@ -364,12 +368,12 @@ describe('ExportMap', () => { ) }) it(`'has' circular reference`, () => { - const result = ExportMap.get('./narcissist', fakeContext) + const result = ExportMap.get('./narcissist', fakeContext)! expect(result).toBeDefined() expect(result.has('soGreat')).toBe(true) }) it(`can 'get' circular reference`, () => { - const result = ExportMap.get('./narcissist', fakeContext) + const result = ExportMap.get('./narcissist', fakeContext)! expect(result).toBeDefined() expect(result.get('soGreat') != null).toBe(true) }) @@ -378,13 +382,10 @@ describe('ExportMap', () => { describe('issue #478: never parse non-whitelist extensions', () => { const context = { ...fakeContext, - settings: { 'import-x/extensions': ['.js'] }, + settings: { 'import-x/extensions': ['.js'] as const }, } - let imports - beforeAll(() => { - imports = ExportMap.get('./typescript.ts', context) - }) + const imports = ExportMap.get('./typescript.ts', context) it('returns nothing for a TypeScript file', () => { expect(imports).toBeFalsy() @@ -392,101 +393,88 @@ describe('ExportMap', () => { }) describe('alternate parsers', () => { - const configs = [ - // ['string form', { 'typescript-eslint-parser': '.ts' }], - ] + describe('array form', () => { + const context = { + ...fakeContext, + settings: { + 'import-x/extensions': ['.js'], + 'import-x/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'] }, + } as const, + } - if (semver.satisfies(eslintPkg.version, '>5')) { - configs.push([ - 'array form', - { '@typescript-eslint/parser': ['.ts', '.tsx'] }, - ]) - } + jest.setTimeout(20e3) // takes a long time :shrug: - configs.forEach(([description, parserConfig]) => { - describe(description, () => { - const context = { - ...fakeContext, - settings: { - 'import-x/extensions': ['.js'], - 'import-x/parsers': parserConfig, - }, - } + const spied = jest.spyOn(getTsconfig, 'getTsconfig').mockClear() - let imports - beforeAll(() => { - jest.setTimeout(20e3) // takes a long time :shrug: - jest.spyOn(getTsconfig, 'getTsconfig').mockClear() - imports = ExportMap.get('./typescript.ts', context) - }) - afterAll(() => { - getTsconfig.getTsconfig.mockRestore() - }) + const imports = ExportMap.get('./typescript.ts', context)! - it('returns something for a TypeScript file', () => { - expect(imports).toBeDefined() - }) + afterAll(() => { + spied.mockRestore() + }) - it('has no parse errors', () => { - expect(imports.errors).toHaveLength(0) - }) + it('returns something for a TypeScript file', () => { + expect(imports).toBeDefined() + }) - it('has exported function', () => { - expect(imports.has('getFoo')).toBe(true) - }) + it('has no parse errors', () => { + expect(imports.errors).toHaveLength(0) + }) - it('has exported typedef', () => { - expect(imports.has('MyType')).toBe(true) - }) + it('has exported function', () => { + expect(imports.has('getFoo')).toBe(true) + }) - it('has exported enum', () => { - expect(imports.has('MyEnum')).toBe(true) - }) + it('has exported typedef', () => { + expect(imports.has('MyType')).toBe(true) + }) - it('has exported interface', () => { - expect(imports.has('Foo')).toBe(true) - }) + it('has exported enum', () => { + expect(imports.has('MyEnum')).toBe(true) + }) - it('has exported abstract class', () => { - expect(imports.has('Bar')).toBe(true) - }) + it('has exported interface', () => { + expect(imports.has('Foo')).toBe(true) + }) - it('should cache tsconfig until tsconfigRootDir parser option changes', () => { - const customContext = { - ...context, - parserOptions: { - tsconfigRootDir: null, - }, - } - expect(getTsconfig.getTsconfig).toHaveBeenCalledTimes(0) - ExportMap.parse('./baz.ts', 'export const baz = 5', customContext) - expect(getTsconfig.getTsconfig).toHaveBeenCalledTimes(1) - ExportMap.parse('./baz.ts', 'export const baz = 5', customContext) - expect(getTsconfig.getTsconfig).toHaveBeenCalledTimes(1) - - const differentContext = { - ...context, - parserOptions: { - tsconfigRootDir: process.cwd(), - }, - } - - ExportMap.parse('./baz.ts', 'export const baz = 5', differentContext) - expect(getTsconfig.getTsconfig).toHaveBeenCalledTimes(2) - }) + it('has exported abstract class', () => { + expect(imports.has('Bar')).toBe(true) + }) - it('should cache after parsing for an ambiguous module', () => { - const source = './typescript-declare-module.ts' - const parseSpy = jest.spyOn(ExportMap, 'parse').mockClear() + it('should cache tsconfig until tsconfigRootDir parser option changes', () => { + const customContext = { + ...context, + parserOptions: { + tsconfigRootDir: null, + }, + } as unknown as ChildContext + expect(getTsconfig.getTsconfig).toHaveBeenCalledTimes(0) + ExportMap.parse('./baz.ts', 'export const baz = 5', customContext) + expect(getTsconfig.getTsconfig).toHaveBeenCalledTimes(1) + ExportMap.parse('./baz.ts', 'export const baz = 5', customContext) + expect(getTsconfig.getTsconfig).toHaveBeenCalledTimes(1) + + const differentContext = { + ...context, + parserOptions: { + tsconfigRootDir: process.cwd(), + }, + } as unknown as ChildContext - expect(ExportMap.get(source, context)).toBeNull() + ExportMap.parse('./baz.ts', 'export const baz = 5', differentContext) + expect(getTsconfig.getTsconfig).toHaveBeenCalledTimes(2) + }) - ExportMap.get(source, context) + it('should cache after parsing for an ambiguous module', () => { + const source = './typescript-declare-module.ts' + const parseSpy = jest.spyOn(ExportMap, 'parse').mockClear() - expect(parseSpy).toHaveBeenCalledTimes(1) + expect(ExportMap.get(source, context)).toBeNull() - parseSpy.mockRestore() - }) + ExportMap.get(source, context) + + expect(parseSpy).toHaveBeenCalledTimes(1) + + parseSpy.mockRestore() }) }) }) diff --git a/test/core/ignore.spec.js b/test/core/ignore.spec.ts similarity index 95% rename from test/core/ignore.spec.js rename to test/core/ignore.spec.ts index 9230fd7b2..bb8d10bd2 100644 --- a/test/core/ignore.spec.js +++ b/test/core/ignore.spec.ts @@ -1,3 +1,4 @@ +import { PluginSettings } from '../../src/types' import { ignore as isIgnored, getFileExtensions, @@ -75,7 +76,7 @@ describe('ignore', () => { }) it('returns a set with the file extensions configured in "import-x/extension"', () => { - const settings = { + const settings: PluginSettings = { 'import-x/extensions': ['.js', '.jsx'], } @@ -86,7 +87,7 @@ describe('ignore', () => { }) it('returns a set with the file extensions configured in "import-x/extension" and "import-x/parsers"', () => { - const settings = { + const settings: PluginSettings = { 'import-x/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'], }, diff --git a/test/core/importType.spec.js b/test/core/import-type.spec.ts similarity index 97% rename from test/core/importType.spec.js rename to test/core/import-type.spec.ts index a805b191f..4a40bcca2 100644 --- a/test/core/importType.spec.js +++ b/test/core/import-type.spec.ts @@ -1,11 +1,11 @@ -import * as path from 'path' +import path from 'path' -import importType, { +import { + importType, isExternalModule, isScoped, isAbsolute, -} from 'core/importType' - +} from '../../src/core/import-type' import { isCoreModule } from '../../src/utils/is-core-module' import { testContext, testFilePath } from '../utils' @@ -330,9 +330,7 @@ describe('importType(name)', () => { 'foo', 'E:\\path\\to\\node_modules\\foo', testContext({ - settings: { - 'import-x/external-module-folders': ['E:\\path\\to\\node_modules'], - }, + 'import-x/external-module-folders': ['E:\\path\\to\\node_modules'], }), ), ).toBe(true) @@ -351,9 +349,7 @@ describe('importType(name)', () => { 'foo', '/path/to/node_modules/foo', testContext({ - settings: { - 'import-x/external-module-folders': ['/path/to/node_modules'], - }, + 'import-x/external-module-folders': ['/path/to/node_modules'], }), ), ).toBe(true) diff --git a/test/core/parseStubParser.js b/test/core/parse-stub-parser.ts similarity index 84% rename from test/core/parseStubParser.js rename to test/core/parse-stub-parser.ts index 10c8e3feb..302df12a2 100644 --- a/test/core/parseStubParser.js +++ b/test/core/parse-stub-parser.ts @@ -1,5 +1,5 @@ // this stub must be in a separate file to require from parse via moduleRequire -module.exports = { +export = { parse() { // }, diff --git a/test/core/parse.spec.js b/test/core/parse.spec.ts similarity index 61% rename from test/core/parse.spec.js rename to test/core/parse.spec.ts index d38a9a8ae..bef92e96f 100644 --- a/test/core/parse.spec.js +++ b/test/core/parse.spec.ts @@ -1,34 +1,30 @@ -import * as fs from 'fs' +import fs from 'fs' import { parse } from '../../src/utils/parse' -import { getFilename } from '../utils' +import { testFilePath } from '../utils' +import { ChildContext, PluginSettings, RuleContext } from '../../src/types' + +import eslintParser from './eslint-parser' +import parseStubParser from './parse-stub-parser' describe('parse(content, { settings, ecmaFeatures })', () => { - const path = getFilename('jsx.js') - const parseStubParser = require('./parseStubParser') - const parseStubParserPath = require.resolve('./parseStubParser') - const eslintParser = require('./eslintParser') - const eslintParserPath = require.resolve('./eslintParser') - let content + const filepath = testFilePath('jsx.js') - beforeAll(done => { - fs.readFile(path, { encoding: 'utf8' }, (err, f) => { - if (err) { - done(err) - } else { - content = f - done() - } - }) - }) + const parseStubParserPath = require.resolve('./parse-stub-parser') + + const eslintParserPath = require.resolve('./eslint-parser') + + const content = fs.readFileSync(filepath, 'utf8') it("doesn't support JSX by default", () => { - expect(() => parse(path, content, { parserPath: 'espree' })).toThrow() + expect(() => + parse(filepath, content, { parserPath: 'espree' } as ChildContext), + ).toThrow() }) it('infers jsx from ecmaFeatures when using stock parser', () => { expect(() => - parse(path, content, { + parse(filepath, content, { settings: {}, parserPath: 'espree', parserOptions: { @@ -36,7 +32,7 @@ describe('parse(content, { settings, ecmaFeatures })', () => { sourceType: 'module', ecmaFeatures: { jsx: true }, }, - }), + } as ChildContext), ).not.toThrow() }) @@ -44,11 +40,11 @@ describe('parse(content, { settings, ecmaFeatures })', () => { const parseSpy = jest.fn() const parserOptions = { ecmaFeatures: { jsx: true } } parseStubParser.parse = parseSpy - parse(path, content, { + parse(filepath, content, { settings: {}, parserPath: parseStubParserPath, parserOptions, - }) + } as ChildContext) // custom parser to be called once expect(parseSpy).toHaveBeenCalledTimes(1) // custom parser to get content as its first argument @@ -68,7 +64,7 @@ describe('parse(content, { settings, ecmaFeatures })', () => { // custom parser to get parserOptions.range equal to true expect(parseSpy.mock.calls[0][1]).toHaveProperty('range', true) // custom parser to get parserOptions.filePath equal to the full path of the source file - expect(parseSpy.mock.calls[0][1]).toHaveProperty('filePath', path) + expect(parseSpy.mock.calls[0][1]).toHaveProperty('filePath', filepath) }) it('passes with custom `parseForESLint` parser', () => { @@ -76,8 +72,11 @@ describe('parse(content, { settings, ecmaFeatures })', () => { .spyOn(eslintParser, 'parseForESLint') .mockClear() const parseSpy = jest.fn() - eslintParser.parse = parseSpy - parse(path, content, { settings: {}, parserPath: eslintParserPath }) + Object.assign(eslintParser, { parse: parseSpy }) + parse(filepath, content, { + settings: {}, + parserPath: eslintParserPath, + } as ChildContext) // custom `parseForESLint` parser to be called once expect(parseForESLintSpy).toHaveBeenCalledTimes(1) // `parseForESLint` takes higher priority than `parse` @@ -85,35 +84,57 @@ describe('parse(content, { settings, ecmaFeatures })', () => { }) it('throws on context == null', () => { - expect(parse.bind(null, path, content, null)).toThrow() + expect( + parse.bind( + null, + filepath, + content, + // @ts-expect-error - testing + null, + ), + ).toThrow() }) it('throws on unable to resolve parserPath', () => { expect( - parse.bind(null, path, content, { settings: {}, parserPath: null }), + parse.bind( + null, + filepath, + content, + // @ts-expect-error - testing + { + settings: {}, + parserPath: null, + }, + ), ).toThrow() }) it('takes the alternate parser specified in settings', () => { - const parseSpy = jest.fn() + jest.spyOn(parseStubParser, 'parse').mockClear() const parserOptions = { ecmaFeatures: { jsx: true } } - parseStubParser.parse = parseSpy expect( - parse.bind(null, path, content, { - settings: { 'import-x/parsers': { [parseStubParserPath]: ['.js'] } }, + parse.bind(null, filepath, content, { + settings: { + 'import-x/parsers': { + [parseStubParserPath]: ['.js'] as const, + }, + // FIXME: it seems a bug in TypeScript + } as PluginSettings, parserPath: null, parserOptions, - }), + } as ChildContext), ).not.toThrow() // custom parser to be called once - expect(parseSpy).toHaveBeenCalledTimes(1) + expect(parseStubParser.parse).toHaveBeenCalledTimes(1) }) it('throws on invalid languageOptions', () => { expect( - parse.bind(null, path, content, { + parse.bind(null, filepath, content, { settings: {}, parserPath: null, + // @ts-expect-error - testing languageOptions: null, }), ).toThrow() @@ -121,60 +142,86 @@ describe('parse(content, { settings, ecmaFeatures })', () => { it('throws on non-object languageOptions.parser', () => { expect( - parse.bind(null, path, content, { + parse.bind(null, filepath, content, { settings: {}, parserPath: null, - languageOptions: { parser: 'espree' }, + languageOptions: { + // @ts-expect-error - testing + parser: 'espree', + }, }), ).toThrow() }) it('throws on null languageOptions.parser', () => { expect( - parse.bind(null, path, content, { + parse.bind(null, filepath, content, { settings: {}, parserPath: null, - languageOptions: { parser: null }, + languageOptions: { + // @ts-expect-error - testing + parser: null, + }, }), ).toThrow() }) it('throws on empty languageOptions.parser', () => { expect( - parse.bind(null, path, content, { + parse.bind(null, filepath, content, { settings: {}, parserPath: null, - languageOptions: { parser: {} }, + languageOptions: { + // @ts-expect-error - testing + parser: {}, + }, }), ).toThrow() }) it('throws on non-function languageOptions.parser.parse', () => { expect( - parse.bind(null, path, content, { + parse.bind(null, filepath, content, { settings: {}, parserPath: null, - languageOptions: { parser: { parse: 'espree' } }, + languageOptions: { + parser: { + // @ts-expect-error - testing + parse: 'espree', + }, + }, }), ).toThrow() }) it('throws on non-function languageOptions.parser.parse', () => { expect( - parse.bind(null, path, content, { + parse.bind(null, filepath, content, { settings: {}, parserPath: null, - languageOptions: { parser: { parseForESLint: 'espree' } }, + languageOptions: { + parser: { + // @ts-expect-error - testing + parseForESLint: 'espree', + }, + }, }), ).toThrow() }) it('requires only one of the parse methods', () => { expect( - parse.bind(null, path, content, { + parse.bind(null, filepath, content, { settings: {}, parserPath: null, - languageOptions: { parser: { parseForESLint: () => ({ ast: {} }) } }, + languageOptions: { + parser: { + parseForESLint: () => ({ + // @ts-expect-error - testing + ast: {}, + }), + }, + }, }), ).not.toThrow() }) @@ -182,10 +229,20 @@ describe('parse(content, { settings, ecmaFeatures })', () => { it('uses parse from languageOptions.parser', () => { const parseSpy = jest.fn() expect( - parse.bind(null, path, content, { - settings: {}, - languageOptions: { parser: { parse: parseSpy } }, - }), + parse.bind( + null, + filepath, + content, + // @ts-expect-error - testing + { + settings: {}, + languageOptions: { + parser: { + parse: parseSpy, + }, + }, + }, + ), ).not.toThrow() // passed parser to be called once expect(parseSpy).toHaveBeenCalledTimes(1) @@ -194,9 +251,14 @@ describe('parse(content, { settings, ecmaFeatures })', () => { it('uses parseForESLint from languageOptions.parser', () => { const parseSpy = jest.fn(() => ({ ast: {} })) expect( - parse.bind(null, path, content, { + parse.bind(null, filepath, content, { settings: {}, - languageOptions: { parser: { parseForESLint: parseSpy } }, + languageOptions: { + parser: { + // @ts-expect-error - testing + parseForESLint: parseSpy, + }, + }, }), ).not.toThrow() // passed parser to be called once @@ -207,11 +269,12 @@ describe('parse(content, { settings, ecmaFeatures })', () => { const parseSpy = jest.fn() parseStubParser.parse = parseSpy expect( - parse.bind(null, path, content, { + parse.bind(null, filepath, content, { settings: { 'import-x/parsers': { [parseStubParserPath]: ['.js'] } }, parserPath: null, languageOptions: { parser: { + // @ts-expect-error - testing parse() { // }, @@ -227,10 +290,13 @@ describe('parse(content, { settings, ecmaFeatures })', () => { const parseSpy = jest.fn() parseStubParser.parse = parseSpy expect( - parse.bind(null, path, content, { + parse.bind(null, filepath, content, { settings: {}, parserPath: 'espree', - languageOptions: { parserOptions: null }, + languageOptions: { + // @ts-expect-error - testing + parserOptions: null, + }, parserOptions: { sourceType: 'module', ecmaVersion: 2015, @@ -244,7 +310,7 @@ describe('parse(content, { settings, ecmaFeatures })', () => { const parseSpy = jest.fn() parseStubParser.parse = parseSpy expect( - parse.bind(null, path, content, { + parse.bind(null, filepath, content, { settings: {}, parserPath: 'espree', languageOptions: { @@ -254,8 +320,10 @@ describe('parse(content, { settings, ecmaFeatures })', () => { ecmaFeatures: { jsx: true }, }, }, - parserOptions: { sourceType: 'script' }, - }), + parserOptions: { + sourceType: 'script', + }, + } as RuleContext), ).not.toThrow() }) }) diff --git a/test/core/resolve.spec.js b/test/core/resolve.spec.ts similarity index 59% rename from test/core/resolve.spec.js rename to test/core/resolve.spec.ts index c7481363d..6607b9a2f 100644 --- a/test/core/resolve.spec.js +++ b/test/core/resolve.spec.ts @@ -1,3 +1,8 @@ +import path from 'path' +import fs from 'fs' +import { setTimeout } from 'timers/promises' + +import type { TSESLint } from '@typescript-eslint/utils' import eslintPkg from 'eslint/package.json' import semver from 'semver' @@ -7,120 +12,121 @@ import { resolve, } from '../../src/utils/resolve' -import path from 'path' -import fs from 'fs' -import * as utils from '../utils' +import { testContext, testFilePath } from '../utils' describe('resolve', () => { it('throws on bad parameters', () => { - expect(resolve.bind(null, null, null)).toThrow() + expect( + resolve.bind( + null, + // @ts-expect-error - testing + null, + null, + ), + ).toThrow() }) it('resolves via a custom resolver with interface version 1', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': './foo-bar-resolver-v1', }) expect( resolve('../fixtures/foo', { - ...testContext, - getFilename() { - return utils.getFilename('foo.js') - }, + ...context, + getFilename: testFilePath, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) expect( resolve('../fixtures/exception', { - ...testContext, - getFilename() { - return utils.getFilename('exception.js') - }, + ...context, + getFilename: () => testFilePath('exception.js'), }), ).toBeUndefined() expect( resolve('../fixtures/not-found', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('not-found.js') + return testFilePath('not-found.js') }, }), ).toBeUndefined() }) it('resolves via a custom resolver with interface version 1 assumed if not specified', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': './foo-bar-resolver-no-version', }) expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('foo.js') + return testFilePath('foo.js') }, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) expect( resolve('../fixtures/exception', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('exception.js') + return testFilePath('exception.js') }, }), ).toBeUndefined() expect( resolve('../fixtures/not-found', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('not-found.js') + return testFilePath('not-found.js') }, }), ).toBeUndefined() }) it('resolves via a custom resolver with interface version 2', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': './foo-bar-resolver-v2', }) - const testContextReports = [] - testContext.report = function (reportInfo) { + const testContextReports: Array> = [] + context.report = reportInfo => { testContextReports.push(reportInfo) } expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('foo.js') + return testFilePath('foo.js') }, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) testContextReports.length = 0 expect( resolve('../fixtures/exception', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('exception.js') + return testFilePath('exception.js') }, }), ).toBeUndefined() expect(testContextReports[0]).toBeInstanceOf(Object) - expect(testContextReports[0].message).toMatch( - 'Resolve error: foo-bar-resolver-v2 resolve test exception', - ) + expect( + 'message' in testContextReports[0] && testContextReports[0].message, + ).toMatch('Resolve error: foo-bar-resolver-v2 resolve test exception') expect(testContextReports[0].loc).toEqual({ line: 1, column: 0 }) testContextReports.length = 0 expect( resolve('../fixtures/not-found', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('not-found.js') + return testFilePath('not-found.js') }, }), ).toBeUndefined() @@ -128,37 +134,37 @@ describe('resolve', () => { }) it('respects import-x/resolver as array of strings', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': ['./foo-bar-resolver-v2', './foo-bar-resolver-v1'], }) expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('foo.js') + return testFilePath('foo.js') }, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) }) it('respects import-x/resolver as object', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': { './foo-bar-resolver-v2': {} }, }) expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('foo.js') + return testFilePath('foo.js') }, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) }) it('respects import-x/resolver as array of objects', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': [ { './foo-bar-resolver-v2': {} }, { './foo-bar-resolver-v1': {} }, @@ -167,104 +173,111 @@ describe('resolve', () => { expect( resolve('../fixtures/foo', { - ...testContext, - getFilename() { - return utils.getFilename('foo.js') - }, + ...context, + getFilename: () => testFilePath('foo.js'), }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) }) it('finds resolvers from the source files rather than ../../../src/utils', () => { - const testContext = utils.testContext({ 'import-x/resolver': { foo: {} } }) + const context = testContext({ 'import-x/resolver': { foo: {} } }) expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('foo.js') + return testFilePath('foo.js') }, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) }) it('reports invalid import-x/resolver config', () => { - const testContext = utils.testContext({ 'import-x/resolver': 123.456 }) - const testContextReports = [] - testContext.report = function (reportInfo) { + const context = testContext({ + // @ts-expect-error - testing + 'import-x/resolver': 123.456, + }) + + const testContextReports: Array> = [] + + context.report = reportInfo => { testContextReports.push(reportInfo) } testContextReports.length = 0 expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('foo.js') + return testFilePath('foo.js') }, }), ).toBeUndefined() expect(testContextReports[0]).toBeInstanceOf(Object) - expect(testContextReports[0].message).toMatch( - 'Resolve error: invalid resolver config', - ) + expect( + 'message' in testContextReports[0] && testContextReports[0].message, + ).toMatch('Resolve error: invalid resolver config') expect(testContextReports[0].loc).toEqual({ line: 1, column: 0 }) }) it('reports loaded resolver with invalid interface', () => { const resolverName = './foo-bar-resolver-invalid' - const testContext = utils.testContext({ 'import-x/resolver': resolverName }) - const testContextReports = [] - testContext.report = function (reportInfo) { + const context = testContext({ 'import-x/resolver': resolverName }) + + const testContextReports: Array> = [] + + context.report = function (reportInfo) { testContextReports.push(reportInfo) } - testContextReports.length = 0 + expect( resolve('../fixtures/foo', { - ...testContext, - getFilename() { - return utils.getFilename('foo.js') - }, + ...context, + getFilename: testFilePath, }), ).toBeUndefined() expect(testContextReports[0]).toBeInstanceOf(Object) - expect(testContextReports[0].message).toMatch( + expect( + 'message' in testContextReports[0] && testContextReports[0].message, + ).toMatch( `Resolve error: ${resolverName} with invalid interface loaded as resolver`, ) expect(testContextReports[0].loc).toEqual({ line: 1, column: 0 }) }) it('respects import-x/resolve extensions', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolve': { extensions: ['.jsx'] }, }) - expect(resolve('./jsx/MyCoolComponent', testContext)).toBe( - utils.testFilePath('./jsx/MyCoolComponent.jsx'), + expect(resolve('./jsx/MyCoolComponent', context)).toBe( + testFilePath('./jsx/MyCoolComponent.jsx'), ) }) it('reports load exception in a user resolver', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': './load-error-resolver', }) - const testContextReports = [] - testContext.report = function (reportInfo) { + + const testContextReports: Array> = [] + + context.report = reportInfo => { testContextReports.push(reportInfo) } expect( resolve('../fixtures/exception', { - ...testContext, + ...context, getFilename() { - return utils.getFilename('exception.js') + return testFilePath('exception.js') }, }), ).toBeUndefined() expect(testContextReports[0]).toBeInstanceOf(Object) - expect(testContextReports[0].message).toMatch( - 'Resolve error: SyntaxError: TEST SYNTAX ERROR', - ) + expect( + 'message' in testContextReports[0] && testContextReports[0].message, + ).toMatch('Resolve error: SyntaxError: TEST SYNTAX ERROR') expect(testContextReports[0].loc).toEqual({ line: 1, column: 0 }) }) @@ -272,134 +285,130 @@ describe('resolve', () => { ;(semver.satisfies(eslintPkg.version, '>= 7.28') ? describe : describe.skip)( 'getPhysicalFilename()', () => { - function unexpectedCallToGetFilename() { + function unexpectedCallToGetFilename(): string { throw new Error( 'Expected to call to getPhysicalFilename() instead of getFilename()', ) } it('resolves via a custom resolver with interface version 1', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': './foo-bar-resolver-v1', }) expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { - return utils.getFilename('foo.js') + return testFilePath('foo.js') }, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) expect( resolve('../fixtures/exception', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { - return utils.getFilename('exception.js') + return testFilePath('exception.js') }, }), ).toBeUndefined() expect( resolve('../fixtures/not-found', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { - return utils.getFilename('not-found.js') + return testFilePath('not-found.js') }, }), ).toBeUndefined() }) it('resolves via a custom resolver with interface version 1 assumed if not specified', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': './foo-bar-resolver-no-version', }) expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { - return utils.getFilename('foo.js') + return testFilePath('foo.js') }, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) expect( resolve('../fixtures/exception', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { - return utils.getFilename('exception.js') + return testFilePath('exception.js') }, }), ).toBeUndefined() expect( resolve('../fixtures/not-found', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { - return utils.getFilename('not-found.js') + return testFilePath('not-found.js') }, }), ).toBeUndefined() }) it('resolves via a custom resolver with interface version 2', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': './foo-bar-resolver-v2', }) - const testContextReports = [] - testContext.report = function (reportInfo) { + const testContextReports: Array> = [] + context.report = reportInfo => { testContextReports.push(reportInfo) } expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, - getPhysicalFilename() { - return utils.getFilename('foo.js') - }, + getPhysicalFilename: testFilePath, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) testContextReports.length = 0 expect( resolve('../fixtures/exception', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { - return utils.getFilename('exception.js') + return testFilePath('exception.js') }, }), ).toBeUndefined() expect(testContextReports[0]).toBeInstanceOf(Object) - expect(testContextReports[0].message).toMatch( - 'Resolve error: foo-bar-resolver-v2 resolve test exception', - ) + expect( + 'message' in testContextReports[0] && testContextReports[0].message, + ).toMatch('Resolve error: foo-bar-resolver-v2 resolve test exception') expect(testContextReports[0].loc).toEqual({ line: 1, column: 0 }) testContextReports.length = 0 expect( resolve('../fixtures/not-found', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, - getPhysicalFilename() { - return utils.getFilename('not-found.js') - }, + getPhysicalFilename: () => testFilePath('not-found.js'), }), ).toBeUndefined() expect(testContextReports.length).toBe(0) }) it('respects import-x/resolver as array of strings', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': [ './foo-bar-resolver-v2', './foo-bar-resolver-v1', @@ -408,33 +417,29 @@ describe('resolve', () => { expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, - getPhysicalFilename() { - return utils.getFilename('foo.js') - }, + getPhysicalFilename: testFilePath, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) }) it('respects import-x/resolver as object', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': { './foo-bar-resolver-v2': {} }, }) expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, - getPhysicalFilename() { - return utils.getFilename('foo.js') - }, + getPhysicalFilename: testFilePath, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) }) it('respects import-x/resolver as array of objects', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': [ { './foo-bar-resolver-v2': {} }, { './foo-bar-resolver-v1': {} }, @@ -443,167 +448,168 @@ describe('resolve', () => { expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, - getPhysicalFilename() { - return utils.getFilename('foo.js') - }, + getPhysicalFilename: testFilePath, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) }) it('finds resolvers from the source files rather than ../../../src/utils', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': { foo: {} }, }) expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, - getPhysicalFilename() { - return utils.getFilename('foo.js') - }, + getPhysicalFilename: testFilePath, }), - ).toBe(utils.testFilePath('./bar.jsx')) + ).toBe(testFilePath('./bar.jsx')) }) it('reports invalid import-x/resolver config', () => { - const testContext = utils.testContext({ 'import-x/resolver': 123.456 }) - const testContextReports = [] - testContext.report = function (reportInfo) { + const context = testContext({ + // @ts-expect-error - testing + 'import-x/resolver': 123.456, + }) + const testContextReports: Array> = [] + context.report = reportInfo => { testContextReports.push(reportInfo) } testContextReports.length = 0 expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, - getPhysicalFilename() { - return utils.getFilename('foo.js') - }, + getPhysicalFilename: testFilePath, }), ).toBeUndefined() expect(testContextReports[0]).toBeInstanceOf(Object) - expect(testContextReports[0].message).toMatch( - 'Resolve error: invalid resolver config', - ) + expect( + 'message' in testContextReports[0] && testContextReports[0].message, + ).toMatch('Resolve error: invalid resolver config') expect(testContextReports[0].loc).toEqual({ line: 1, column: 0 }) }) it('reports loaded resolver with invalid interface', () => { const resolverName = './foo-bar-resolver-invalid' - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': resolverName, }) - const testContextReports = [] - testContext.report = function (reportInfo) { + const testContextReports: Array> = [] + context.report = reportInfo => { testContextReports.push(reportInfo) } - testContextReports.length = 0 expect( resolve('../fixtures/foo', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, - getPhysicalFilename() { - return utils.getFilename('foo.js') - }, + getPhysicalFilename: testFilePath, }), ).toBeUndefined() expect(testContextReports[0]).toBeInstanceOf(Object) - expect(testContextReports[0].message).toMatch( + expect( + 'message' in testContextReports[0] && testContextReports[0].message, + ).toMatch( `Resolve error: ${resolverName} with invalid interface loaded as resolver`, ) expect(testContextReports[0].loc).toEqual({ line: 1, column: 0 }) }) it('respects import-x/resolve extensions', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolve': { extensions: ['.jsx'] }, }) - expect(resolve('./jsx/MyCoolComponent', testContext)).toBe( - utils.testFilePath('./jsx/MyCoolComponent.jsx'), + expect(resolve('./jsx/MyCoolComponent', context)).toBe( + testFilePath('./jsx/MyCoolComponent.jsx'), ) }) it('reports load exception in a user resolver', () => { - const testContext = utils.testContext({ + const context = testContext({ 'import-x/resolver': './load-error-resolver', }) - const testContextReports = [] - testContext.report = function (reportInfo) { + const testContextReports: Array> = [] + context.report = reportInfo => { testContextReports.push(reportInfo) } expect( resolve('../fixtures/exception', { - ...testContext, + ...context, getFilename: unexpectedCallToGetFilename, - getPhysicalFilename() { - return utils.getFilename('exception.js') - }, + getPhysicalFilename: () => testFilePath('exception.js'), }), ).toBeUndefined() expect(testContextReports[0]).toBeInstanceOf(Object) - expect(testContextReports[0].message).toMatch( - 'Resolve error: SyntaxError: TEST SYNTAX ERROR', - ) + expect( + 'message' in testContextReports[0] && testContextReports[0].message, + ).toMatch('Resolve error: SyntaxError: TEST SYNTAX ERROR') expect(testContextReports[0].loc).toEqual({ line: 1, column: 0 }) }) }, ) - const caseDescribe = !CASE_SENSITIVE_FS ? describe : describe.skip - caseDescribe('case sensitivity', function () { - let file - const testContext = utils.testContext({ + const caseDescribe = CASE_SENSITIVE_FS ? describe.skip : describe + + caseDescribe('case sensitivity', () => { + const context = testContext({ 'import-x/resolve': { extensions: ['.jsx'] }, 'import-x/cache': { lifetime: 0 }, }) - const testSettings = testContext.settings - beforeAll(() => { - file = resolve( - // Note the case difference 'MyUncoolComponent' vs 'MyUnCoolComponent' - './jsx/MyUncoolComponent', - testContext, - ) - }) + + const cacheSettings = context.settings['import-x/cache'] + + const file = resolve( + // Note the case difference 'MyUncoolComponent' vs 'MyUnCoolComponent' + './jsx/MyUncoolComponent', + context, + )! + it('resolves regardless of case', () => { // path to ./jsx/MyUncoolComponent expect(file).toBeDefined() }) + it('detects case does not match FS', () => { - expect(fileExistsWithCaseSync(file, testSettings)).toBe(false) + expect(fileExistsWithCaseSync(file, cacheSettings)).toBe(false) }) + it('detecting case does not include parent folder path (issue #720)', () => { const f = path.join( process.cwd().toUpperCase(), './test/fixtures/jsx/MyUnCoolComponent.jsx', ) - expect(fileExistsWithCaseSync(f, testSettings)).toBe(true) + expect(fileExistsWithCaseSync(f, cacheSettings)).toBe(true) }) + it('detecting case should include parent folder path', () => { const f = path.join( process.cwd().toUpperCase(), './test/fixtures/jsx/MyUnCoolComponent.jsx', ) - expect(fileExistsWithCaseSync(f, testSettings, true)).toBe(false) + expect(fileExistsWithCaseSync(f, cacheSettings, true)).toBe(false) }) }) describe('rename cache correctness', () => { - const context = utils.testContext({ + const context = testContext({ 'import-x/cache': { lifetime: 1 }, }) - const infiniteContexts = ['∞', 'Infinity'].map(inf => [ - inf, - utils.testContext({ - 'import-x/cache': { lifetime: inf }, - }), - ]) + const infiniteContexts = (['∞', 'Infinity'] as const).map( + inf => + [ + inf, + testContext({ + 'import-x/cache': { lifetime: inf }, + }), + ] as const, + ) const pairs = [['./CaseyKasem.js', './CASEYKASEM2.js']] @@ -612,27 +618,26 @@ describe('resolve', () => { beforeAll(() => { expect(resolve(original, context)).toBeDefined() expect(resolve(changed, context)).toBeFalsy() - }) - // settings are part of cache key - beforeAll(() => { + // settings are part of cache key infiniteContexts.forEach(([, c]) => { expect(resolve(original, c)).toBeDefined() }) }) - beforeAll(done => { - fs.rename( - utils.testFilePath(original), - utils.testFilePath(changed), - done, - ) + beforeAll(() => + fs.promises.rename(testFilePath(original), testFilePath(changed)), + ) + + beforeAll(() => { + const exists = fs.existsSync(testFilePath(changed)) + if (!exists) { + throw new Error('new file does not exist') + } }) - beforeAll(done => - fs.exists(utils.testFilePath(changed), exists => - done(exists ? null : new Error('new file does not exist')), - ), + afterAll(() => + fs.promises.rename(testFilePath(changed), testFilePath(original)), ) it('gets cached values within cache lifetime', () => { @@ -649,9 +654,7 @@ describe('resolve', () => { describe('infinite cache', () => { jest.setTimeout(1.5e3) - beforeAll(done => { - setTimeout(done, 1100) - }) + beforeAll(() => setTimeout(1100)) infiniteContexts.forEach(([inf, infiniteContext]) => { it(`lifetime: ${inf} still gets cached values after ~1s`, () => { @@ -663,22 +666,12 @@ describe('resolve', () => { describe('finite cache', () => { jest.setTimeout(1.2e3) - beforeAll(done => { - setTimeout(done, 1000) - }) + beforeAll(() => setTimeout(1000)) it('gets correct values after cache lifetime', () => { expect(resolve(original, context)).toBeFalsy() expect(resolve(changed, context)).toBeDefined() }) }) - - afterAll(done => { - fs.rename( - utils.testFilePath(changed), - utils.testFilePath(original), - done, - ) - }) }) }) }) diff --git a/test/package.spec.js b/test/package.spec.js deleted file mode 100644 index e413b8876..000000000 --- a/test/package.spec.js +++ /dev/null @@ -1,82 +0,0 @@ -const path = require('path') -const fs = require('fs') - -import { srcDir } from './utils' - -/** - * @param {string} f - * @returns {boolean} - */ -function isSourceFile(f) { - const ext = path.extname(f) - return ext === '.js' || (ext === '.ts' && !f.endsWith('.d.ts')) -} - -describe('package', () => { - const pkg = path.resolve(srcDir) - - let module - - beforeAll(() => { - module = require(pkg) - }) - - it('exists', () => { - expect(module).toBeDefined() - }) - - it('has every rule', done => { - fs.readdir(path.join(pkg, 'rules'), function (err, files) { - expect(err).toBeFalsy() - - files.filter(isSourceFile).forEach(function (f) { - expect(module.rules).toHaveProperty(path.basename(f, path.extname(f))) - }) - - done() - }) - }) - - it('exports all configs', done => { - fs.readdir(path.resolve(srcDir, 'config'), function (err, files) { - if (err) { - done(err) - return - } - files.filter(isSourceFile).forEach(file => { - expect(module.configs).toHaveProperty( - path.basename(file, path.extname(file)), - ) - }) - done() - }) - }) - - it('has configs only for rules that exist', () => { - for (const configFile in module.configs) { - const preamble = 'import-x/' - - for (const rule in module.configs[configFile].rules) { - expect(() => - require(getRulePath(rule.slice(preamble.length))), - ).not.toThrow() - } - } - - function getRulePath(ruleName) { - // 'require' does not work with dynamic paths because of the compilation step by babel - // (which resolves paths according to the root folder configuration) - // the usage of require.resolve on a static path gets around this - return path.resolve( - require.resolve('rules/no-unresolved'), - '..', - ruleName, - ) - } - }) - - it('marks deprecated rules in their metadata', () => { - expect(module.rules['imports-first'].meta.deprecated).toBe(true) - expect(module.rules.first.meta.deprecated).not.toBe(true) - }) -}) diff --git a/test/package.spec.ts b/test/package.spec.ts new file mode 100644 index 000000000..75876072e --- /dev/null +++ b/test/package.spec.ts @@ -0,0 +1,65 @@ +import path from 'path' +import fs from 'fs/promises' + +import type { TSESLint } from '@typescript-eslint/utils' + +import { moduleRequire, pluginName } from '../src/utils' +import { srcDir } from './utils' + +function isSourceFile(f: string) { + const ext = path.extname(f) + return ext === '.js' || (ext === '.ts' && !f.endsWith('.d.ts')) +} + +describe('package', () => { + const pkg = path.resolve(srcDir) + + const module = moduleRequire< + TSESLint.Linter.Plugin & { + rules: Record> + } + >(pkg) + + it('exists', () => { + expect(module).toBeDefined() + }) + + it('has every rule', async () => { + const files = await fs.readdir(path.resolve(pkg, 'rules')) + files.filter(isSourceFile).forEach(f => { + expect(module.rules).toHaveProperty(path.basename(f, path.extname(f))) + }) + }) + + it('exports all configs', async () => { + const files = await fs.readdir(path.resolve(srcDir, 'config')) + files.filter(isSourceFile).forEach(file => { + expect(module.configs).toHaveProperty( + path.basename(file, path.extname(file)), + ) + }) + }) + + it('has configs only for rules that exist', () => { + const preamble = `${pluginName}/` + for (const config of Object.values(module.configs!)) { + if (!config.rules) { + continue + } + for (const rule of Object.keys(config.rules)) { + expect(() => + require(getRulePath(rule.slice(preamble.length))), + ).not.toThrow() + } + } + + function getRulePath(ruleName: string) { + return path.resolve(srcDir, 'rules', ruleName) + } + }) + + it('marks deprecated rules in their metadata', () => { + expect(module.rules!['imports-first'].meta.deprecated).toBe(true) + expect(module.rules!.first.meta.deprecated).not.toBe(true) + }) +}) diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 000000000..607f79a97 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "paths": { + "eslint-plugin-import-x": ["./src"] + } + } +} diff --git a/test/utils.js b/test/utils.ts similarity index 76% rename from test/utils.js rename to test/utils.ts index 16249b400..6aad6967e 100644 --- a/test/utils.js +++ b/test/utils.ts @@ -1,8 +1,12 @@ import path from 'path' + +import { TSESLint } from '@typescript-eslint/utils' import eslintPkg from 'eslint/package.json' import semver from 'semver' import typescriptPkg from 'typescript/package.json' +import type { PluginSettings, RuleContext } from '../src/types' + // warms up the module cache. this import takes a while (>500ms) import '@babel/eslint-parser' @@ -12,39 +16,38 @@ export const parsers = { BABEL: require.resolve('@babel/eslint-parser'), } -export function tsVersionSatisfies(specifier) { +export function tsVersionSatisfies(specifier: string) { return semver.satisfies(typescriptPkg.version, specifier) } -export function typescriptEslintParserSatisfies(specifier) { - return ( - parsers.TS && - semver.satisfies( - require('@typescript-eslint/parser/package.json').version, - specifier, - ) +export function typescriptEslintParserSatisfies(specifier: string) { + return semver.satisfies( + require('@typescript-eslint/parser/package.json').version, + specifier, ) } -export function testFilePath(relativePath) { - return path.join(process.cwd(), './test/fixtures', relativePath) +export function testFilePath(relativePath = 'foo.js') { + return path.resolve('test/fixtures', relativePath) } export function getNonDefaultParsers() { - return [parsers.TS, parsers.BABEL].filter(Boolean) + return [parsers.TS, parsers.BABEL] } -export const FILENAME = testFilePath('foo.js') +const FILENAME = testFilePath() -export function eslintVersionSatisfies(specifier) { +export function eslintVersionSatisfies(specifier: string) { return semver.satisfies(eslintPkg.version, specifier) } -export function testVersion(specifier, t) { +type ValidTestCase = TSESLint.ValidTestCase + +export function testVersion(specifier: string, t: () => ValidTestCase) { return eslintVersionSatisfies(specifier) ? test(t()) : [] } -export function test(t) { +export function test(t: ValidTestCase): ValidTestCase { if (arguments.length !== 1) { throw new SyntaxError('`test` requires exactly one object argument') } @@ -59,23 +62,21 @@ export function test(t) { } } -export function testContext(settings) { +export function testContext(settings?: PluginSettings) { return { getFilename() { return FILENAME }, settings: settings || {}, - } + } as RuleContext } -export function getFilename(file) { - return path.join(__dirname, 'fixtures', file || 'foo.js') -} +// TODO: remove this alias +export const getFilename = testFilePath /** * to be added as valid cases just to ensure no nullable fields are going * to crash at runtime - * @type {Array} */ export const SYNTAX_CASES = [ test({ code: 'for (let { foo, bar } of baz) {}' }), diff --git a/tsconfig.base.json b/tsconfig.base.json index 631a3f1ae..9d678bd29 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,9 +1,7 @@ { "extends": "@1stg/tsconfig/node", "compilerOptions": { - "allowJs": true, - "paths": { - "eslint-plugin-import-x": ["./src"] - } - } + "allowJs": true + }, + "exclude": ["test/fixtures"] } diff --git a/yarn.lock b/yarn.lock index b5516ca0e..01255d4a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1909,7 +1909,14 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== -"@types/node@*", "@types/node@^12.7.1": +"@types/node@*", "@types/node@^20.11.26": + version "20.11.26" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.26.tgz#3fbda536e51d5c79281e1d9657dcb0131baabd2d" + integrity sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ== + dependencies: + undici-types "~5.26.4" + +"@types/node@^12.7.1": version "12.20.55" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== @@ -2139,7 +2146,7 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -ansi-styles@^6.1.0: +ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -4395,6 +4402,11 @@ json-parse-even-better-errors@^2.3.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-parse-even-better-errors@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz#02bb29fb5da90b5444581749c22cedd3597c6cb0" + integrity sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -4643,6 +4655,11 @@ memory-fs@^0.2.0: resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" integrity sha512-+y4mDxU4rvXXu5UDSGCGNiesFmwCHuefGMoPCO1WYucNYj7DsLqrFaa2fXVI0H+NNiPTwwzKwspn9yTZqUGqng== +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== + meow@^6.0.0: version "6.1.1" resolved "https://registry.yarnpkg.com/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" @@ -4695,7 +4712,7 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.1, minimatch@^9.0.3: +minimatch@^9.0.0, minimatch@^9.0.1, minimatch@^9.0.3: version "9.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== @@ -4786,6 +4803,24 @@ normalize-path@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +npm-normalize-package-bin@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz#25447e32a9a7de1f51362c61a559233b89947832" + integrity sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ== + +npm-run-all2@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/npm-run-all2/-/npm-run-all2-6.1.2.tgz#637b2b804f32dd8cee9e5edf7d47a9fc4ca8bf9d" + integrity sha512-WwwnS8Ft+RpXve6T2EIEVpFLSqN+ORHRvgNk3H9N62SZXjmzKoRhMFg3I17TK3oMaAEr+XFbRirWS2Fn3BCPSg== + dependencies: + ansi-styles "^6.2.1" + cross-spawn "^7.0.3" + memorystream "^0.3.1" + minimatch "^9.0.0" + pidtree "^0.6.0" + read-package-json-fast "^3.0.2" + shell-quote "^1.7.3" + npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -4988,6 +5023,11 @@ picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pidtree@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -5156,6 +5196,14 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +read-package-json-fast@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz#394908a9725dc7a5f14e70c8e7556dff1d2b1049" + integrity sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw== + dependencies: + json-parse-even-better-errors "^3.0.0" + npm-normalize-package-bin "^3.0.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" @@ -5460,6 +5508,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shell-quote@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + side-channel@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" @@ -5963,6 +6016,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"