Skip to content

Commit 8bd40d8

Browse files
authored
refactor: drop @typescript-eslint/utils on production (#362)
1 parent 3848fe2 commit 8bd40d8

18 files changed

+375
-29
lines changed

.changeset/legal-squids-cover.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-import-x": minor
3+
---
4+
5+
refactor: drop @typescript-eslint/utils on production

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,20 @@
6464
"watch": "yarn test --watch"
6565
},
6666
"peerDependencies": {
67+
"@typescript-eslint/utils": "^8.0.0",
6768
"eslint": "^8.57.0 || ^9.0.0",
6869
"eslint-import-resolver-node": "*"
6970
},
7071
"peerDependenciesMeta": {
72+
"@typescript-eslint/utils": {
73+
"optional": true
74+
},
7175
"eslint-import-resolver-node": {
7276
"optional": true
7377
}
7478
},
7579
"dependencies": {
76-
"@typescript-eslint/utils": "^8.33.0",
80+
"@typescript-eslint/types": "^8.33.0",
7781
"comment-parser": "^1.4.1",
7882
"debug": "^4.4.1",
7983
"eslint-import-context": "^0.1.6",
@@ -123,6 +127,7 @@
123127
"@typescript-eslint/eslint-plugin": "^8.33.0",
124128
"@typescript-eslint/parser": "^8.33.0",
125129
"@typescript-eslint/rule-tester": "^8.33.0",
130+
"@typescript-eslint/utils": "^8.33.0",
126131
"@unts/patch-package": "^8.1.1",
127132
"clean-pkg-json": "^1.3.0",
128133
"eslint": "^9.27.0",

src/rules/dynamic-import-chunkname.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import vm from 'node:vm'
22

3-
import type { TSESTree } from '@typescript-eslint/utils'
4-
import type { RuleFixer } from '@typescript-eslint/utils/ts-eslint'
3+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils'
54

65
import { createRule } from '../utils/index.js'
76

@@ -146,7 +145,7 @@ export default createRule<[Options?], MessageId>({
146145
}
147146

148147
const removeCommentsAndLeadingSpaces = (
149-
fixer: RuleFixer,
148+
fixer: TSESLint.RuleFixer,
150149
comment: TSESTree.Comment,
151150
) => {
152151
const leftToken = sourceCode.getTokenBefore(comment)

src/rules/export.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
1+
import { AST_NODE_TYPES } from '@typescript-eslint/types'
22
import type { TSESTree } from '@typescript-eslint/utils'
33

44
import {

src/rules/extensions.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import path from 'node:path'
22

3-
import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'
4-
import type { RuleFixer } from '@typescript-eslint/utils/ts-eslint'
3+
import type { JSONSchema, TSESLint } from '@typescript-eslint/utils'
54
import { minimatch } from 'minimatch'
65
import type { MinimatchOptions } from 'minimatch'
76

@@ -22,12 +21,12 @@ const modifierValues = ['always', 'ignorePackages', 'never'] as const
2221
const modifierSchema = {
2322
type: 'string',
2423
enum: [...modifierValues],
25-
} satisfies JSONSchema4
24+
} satisfies JSONSchema.JSONSchema4
2625

2726
const modifierByFileExtensionSchema = {
2827
type: 'object',
2928
patternProperties: { '.*': modifierSchema },
30-
} satisfies JSONSchema4
29+
} satisfies JSONSchema.JSONSchema4
3130

3231
const properties = {
3332
type: 'object',
@@ -59,7 +58,7 @@ const properties = {
5958
type: 'boolean',
6059
},
6160
},
62-
} satisfies JSONSchema4
61+
} satisfies JSONSchema.JSONSchema4
6362

6463
export type Modifier = (typeof modifierValues)[number]
6564

@@ -370,7 +369,7 @@ export default createRule<Options, MessageId>({
370369
hash,
371370
})
372371
const fixOrSuggest = {
373-
fix(fixer: RuleFixer) {
372+
fix(fixer: TSESLint.RuleFixer) {
374373
return fixer.replaceText(
375374
source,
376375
replaceImportPath(source.raw, fixedImportPath),
@@ -415,7 +414,7 @@ export default createRule<Options, MessageId>({
415414
hash,
416415
})
417416
const fixOrSuggest = {
418-
fix(fixer: RuleFixer) {
417+
fix(fixer: TSESLint.RuleFixer) {
419418
return fixer.replaceText(
420419
source,
421420
replaceImportPath(source.raw, fixedImportPath),
@@ -445,7 +444,7 @@ export default createRule<Options, MessageId>({
445444
commonSuggestion,
446445
isIndex && {
447446
...commonSuggestion,
448-
fix(fixer: RuleFixer) {
447+
fix(fixer: TSESLint.RuleFixer) {
449448
return fixer.replaceText(
450449
source,
451450
replaceImportPath(

src/rules/no-unused-modules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import path from 'node:path'
77

8-
import { TSESTree } from '@typescript-eslint/utils'
8+
import { TSESTree } from '@typescript-eslint/types'
99
import type { TSESLint } from '@typescript-eslint/utils'
1010
// eslint-disable-next-line import-x/default -- incorrect types , commonjs actually
1111
import eslintUnsupportedApi from 'eslint/use-at-your-own-risk'

src/utils/apply-default.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { deepMerge, isObjectNotArray } from './deep-merge.js'
2+
3+
/**
4+
* Pure function - doesn't mutate either parameter! Uses the default options and
5+
* overrides with the options provided by the user
6+
*
7+
* @param defaultOptions The defaults
8+
* @param userOptions The user opts
9+
* @returns The options with defaults
10+
*/
11+
export function applyDefault<
12+
User extends readonly unknown[],
13+
Default extends User,
14+
>(
15+
defaultOptions: Readonly<Default>,
16+
userOptions: Readonly<User> | null,
17+
): Default {
18+
// clone defaults
19+
const options = structuredClone(defaultOptions) as AsMutable<Default>
20+
21+
if (userOptions == null) {
22+
return options
23+
}
24+
25+
// For avoiding the type error
26+
// `This expression is not callable. Type 'unknown' has no call signatures.ts(2349)`
27+
for (const [i, opt] of (options as unknown[]).entries()) {
28+
if (userOptions[i] !== undefined) {
29+
const userOpt = userOptions[i]
30+
options[i] =
31+
isObjectNotArray(userOpt) && isObjectNotArray(opt)
32+
? deepMerge(opt, userOpt)
33+
: userOpt
34+
}
35+
}
36+
37+
return options
38+
}
39+
40+
type AsMutable<T extends readonly unknown[]> = {
41+
-readonly [Key in keyof T]: T[Key]
42+
}

src/utils/create-rule.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,70 @@
1-
import { ESLintUtils } from '@typescript-eslint/utils'
1+
import type { ESLintUtils, TSESLint } from '@typescript-eslint/utils'
22

3+
import { applyDefault } from './apply-default.js'
34
import { docsUrl } from './docs-url.js'
45

6+
/**
7+
* Creates reusable function to create rules with default options and docs URLs.
8+
*
9+
* @param urlCreator Creates a documentation URL for a given rule name.
10+
* @returns Function to create a rule with the docs URL format.
11+
*/
12+
export function RuleCreator<PluginDocs = unknown>(
13+
urlCreator: (ruleName: string) => string,
14+
) {
15+
// This function will get much easier to call when this is merged https://github.com/Microsoft/TypeScript/pull/26349
16+
// TODO - when the above PR lands; add type checking for the context.report `data` property
17+
return function createNamedRule<
18+
Options extends readonly unknown[],
19+
MessageIds extends string,
20+
>({
21+
meta,
22+
name,
23+
...rule
24+
}: Readonly<
25+
ESLintUtils.RuleWithMetaAndName<Options, MessageIds, PluginDocs>
26+
>): TSESLint.RuleModule<MessageIds, Options, PluginDocs> {
27+
return createRule_<Options, MessageIds, PluginDocs>({
28+
meta: {
29+
...meta,
30+
docs: {
31+
...meta.docs,
32+
url: urlCreator(name),
33+
},
34+
},
35+
...rule,
36+
})
37+
}
38+
}
39+
40+
function createRule_<
41+
Options extends readonly unknown[],
42+
MessageIds extends string,
43+
PluginDocs = unknown,
44+
>({
45+
create,
46+
defaultOptions,
47+
meta,
48+
}: Readonly<
49+
ESLintUtils.RuleWithMeta<Options, MessageIds, PluginDocs>
50+
>): TSESLint.RuleModule<MessageIds, Options, PluginDocs> {
51+
return {
52+
create(
53+
context: Readonly<TSESLint.RuleContext<MessageIds, Options>>,
54+
): TSESLint.RuleListener {
55+
const optionsWithDefault = applyDefault(defaultOptions, context.options)
56+
return create(context, optionsWithDefault)
57+
},
58+
defaultOptions,
59+
meta,
60+
}
61+
}
62+
563
export interface ImportXPluginDocs {
664
/** The category the rule falls under */
765
category?: string
866

967
recommended?: true
1068
}
1169

12-
export const createRule = ESLintUtils.RuleCreator<ImportXPluginDocs>(docsUrl)
70+
export const createRule = RuleCreator<ImportXPluginDocs>(docsUrl)

src/utils/deep-merge.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export type ObjectLike<T = unknown> = Record<string, T>
2+
3+
/**
4+
* Check if the variable contains an object strictly rejecting arrays
5+
*
6+
* @returns `true` if obj is an object
7+
*/
8+
export function isObjectNotArray(obj: unknown): obj is ObjectLike {
9+
return typeof obj === 'object' && obj != null && !Array.isArray(obj)
10+
}
11+
12+
/**
13+
* Pure function - doesn't mutate either parameter! Merges two objects together
14+
* deeply, overwriting the properties in first with the properties in second
15+
*
16+
* @param first The first object
17+
* @param second The second object
18+
* @returns A new object
19+
*/
20+
export function deepMerge(
21+
first: ObjectLike = {},
22+
second: ObjectLike = {},
23+
): Record<string, unknown> {
24+
// get the unique set of keys across both objects
25+
const keys = new Set([...Object.keys(first), ...Object.keys(second)])
26+
27+
return Object.fromEntries(
28+
[...keys].map(key => {
29+
const firstHasKey = key in first
30+
const secondHasKey = key in second
31+
const firstValue = first[key]
32+
const secondValue = second[key]
33+
34+
let value
35+
if (firstHasKey && secondHasKey) {
36+
value =
37+
// object type
38+
isObjectNotArray(firstValue) && isObjectNotArray(secondValue)
39+
? deepMerge(firstValue, secondValue)
40+
: // value type
41+
secondValue
42+
} else if (firstHasKey) {
43+
value = firstValue
44+
} else {
45+
value = secondValue
46+
}
47+
return [key, value]
48+
}),
49+
)
50+
}

src/utils/get-value.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { TSESTree } from '@typescript-eslint/utils'
1+
import { TSESTree } from '@typescript-eslint/types'
22

33
export const getValue = (
44
node: TSESTree.Identifier | TSESTree.StringLiteral,

0 commit comments

Comments
 (0)