@mrpalmer/eslint-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/constants.d.ts +2 -0
- package/lib/constants.js +1 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +31 -0
- package/lib/meta.d.ts +4 -0
- package/lib/meta.js +17 -0
- package/lib/rules/shorten-paths.d.ts +8 -0
- package/lib/rules/shorten-paths.js +70 -0
- package/lib/rules/sort-imports.d.ts +19 -0
- package/lib/rules/sort-imports.js +636 -0
- package/lib/rules/sort-named.d.ts +8 -0
- package/lib/rules/sort-named.js +436 -0
- package/lib/types.d.ts +12 -0
- package/lib/types.js +1 -0
- package/lib/utils/array.d.ts +6 -0
- package/lib/utils/array.js +23 -0
- package/lib/utils/ast.d.ts +4 -0
- package/lib/utils/ast.js +102 -0
- package/lib/utils/create-rule.d.ts +2 -0
- package/lib/utils/create-rule.js +58 -0
- package/lib/utils/import-path-options.d.ts +6 -0
- package/lib/utils/import-path-options.js +98 -0
- package/lib/utils/import-type.d.ts +3 -0
- package/lib/utils/import-type.js +88 -0
- package/lib/utils/settings.d.ts +3 -0
- package/lib/utils/settings.js +20 -0
- package/lib/utils/shortest-path.d.ts +2 -0
- package/lib/utils/shortest-path.js +45 -0
- package/lib/utils/ts-config.d.ts +16 -0
- package/lib/utils/ts-config.js +54 -0
- package/package.json +31 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createPathsMatcher } from 'get-tsconfig';
|
|
4
|
+
export function getImportPathOptions({ originalPath, filePath, tsconfig, }) {
|
|
5
|
+
const options = new Set([originalPath]);
|
|
6
|
+
if (isRelative(originalPath)) {
|
|
7
|
+
options.add(formatRelativeImportPath(filePath, originalPath));
|
|
8
|
+
}
|
|
9
|
+
// if there's no tsconfig, we can't use aliases
|
|
10
|
+
if (!tsconfig) {
|
|
11
|
+
return options;
|
|
12
|
+
}
|
|
13
|
+
const pathsMatcher = createPathsMatcher(tsconfig);
|
|
14
|
+
const { baseUrl, paths } = tsconfig.config.compilerOptions ?? {};
|
|
15
|
+
const baseUrlAbsolutePath = path.resolve(path.dirname(tsconfig.path), baseUrl ?? '.');
|
|
16
|
+
// see if there's a relative path match
|
|
17
|
+
const relativeMatch = resolveAlias(originalPath, pathsMatcher);
|
|
18
|
+
if (relativeMatch) {
|
|
19
|
+
const relative = formatRelativeImportPath(filePath, relativeMatch);
|
|
20
|
+
options.add(relative);
|
|
21
|
+
// check if there's a better alias match
|
|
22
|
+
if (paths) {
|
|
23
|
+
const aliases = findAliases({
|
|
24
|
+
filePath,
|
|
25
|
+
originalImportPath: relative,
|
|
26
|
+
baseUrlAbsolutePath,
|
|
27
|
+
paths,
|
|
28
|
+
pathsMatcher,
|
|
29
|
+
});
|
|
30
|
+
for (const alias of aliases) {
|
|
31
|
+
options.add(alias);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!paths) {
|
|
36
|
+
return options;
|
|
37
|
+
}
|
|
38
|
+
// see if there's an alias match
|
|
39
|
+
if (isRelative(originalPath)) {
|
|
40
|
+
const aliases = findAliases({
|
|
41
|
+
filePath,
|
|
42
|
+
originalImportPath: originalPath,
|
|
43
|
+
baseUrlAbsolutePath,
|
|
44
|
+
paths,
|
|
45
|
+
pathsMatcher,
|
|
46
|
+
});
|
|
47
|
+
for (const alias of aliases) {
|
|
48
|
+
options.add(alias);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return options;
|
|
52
|
+
}
|
|
53
|
+
function isRelative(testPath) {
|
|
54
|
+
return /^\.{1,2}\//.test(testPath);
|
|
55
|
+
}
|
|
56
|
+
function formatRelativeImportPath(currentFile, relativePath) {
|
|
57
|
+
const currentDir = path.dirname(currentFile);
|
|
58
|
+
const absolutePath = path.resolve(currentDir, relativePath);
|
|
59
|
+
const relative = path.relative(currentDir, absolutePath);
|
|
60
|
+
return isRelative(relative) ? relative : `./${relative}`;
|
|
61
|
+
}
|
|
62
|
+
function findAliases({ filePath, originalImportPath, baseUrlAbsolutePath, paths, pathsMatcher, }) {
|
|
63
|
+
const options = new Set();
|
|
64
|
+
const directory = path.dirname(filePath);
|
|
65
|
+
const absolutePath = path.resolve(directory, originalImportPath);
|
|
66
|
+
const relativeToBase = path.relative(baseUrlAbsolutePath, absolutePath);
|
|
67
|
+
for (const alias of Object.keys(paths)) {
|
|
68
|
+
const prefix = alias.replace(/\/\*$/, '');
|
|
69
|
+
const tryPaths = paths[alias];
|
|
70
|
+
for (const tryPath of tryPaths) {
|
|
71
|
+
const partialPath = tryPath === '*' ? '' : tryPath.replace(/\/\*$/, '');
|
|
72
|
+
const relative = path.relative(partialPath, relativeToBase);
|
|
73
|
+
if (!relative.startsWith('..')) {
|
|
74
|
+
const alias = path.join(prefix, relative);
|
|
75
|
+
if (resolveAlias(alias, pathsMatcher) != null) {
|
|
76
|
+
options.add(path.join(prefix, relative));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return options;
|
|
82
|
+
}
|
|
83
|
+
const importExtensionMappings = {
|
|
84
|
+
'': ['.js', '.ts', '.jsx', '.tsx'],
|
|
85
|
+
'.js': ['.ts', '.js'],
|
|
86
|
+
};
|
|
87
|
+
function fileExistsAtPath(filePath) {
|
|
88
|
+
const { ext } = path.parse(filePath);
|
|
89
|
+
const candidateExtensions = importExtensionMappings[ext] ?? [ext];
|
|
90
|
+
return candidateExtensions.some((ext) => fs.existsSync(filePath + ext));
|
|
91
|
+
}
|
|
92
|
+
function resolveAlias(importPath, matcher) {
|
|
93
|
+
if (!matcher) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const matches = matcher(importPath);
|
|
97
|
+
return matches.find((match) => fileExistsAtPath(match));
|
|
98
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { LiteralNodeValue, PluginSettings } from '../types.js';
|
|
2
|
+
export type ImportType = Exclude<ReturnType<typeof importType>, `core:${string}`>;
|
|
3
|
+
export declare function importType(name: LiteralNodeValue, settings: PluginSettings): "relative" | "unknown" | "internal" | "absolute" | "builtin" | `core:${string}` | "index" | "sibling" | "external";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { isBuiltin } from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { minimatch } from 'minimatch';
|
|
4
|
+
export function importType(name, settings) {
|
|
5
|
+
if (typeof name !== 'string') {
|
|
6
|
+
return 'unknown';
|
|
7
|
+
}
|
|
8
|
+
if (isInternalRegexMatch(name, settings)) {
|
|
9
|
+
return 'internal';
|
|
10
|
+
}
|
|
11
|
+
if (isAbsolute(name)) {
|
|
12
|
+
return 'absolute';
|
|
13
|
+
}
|
|
14
|
+
if (isBuiltIn(name)) {
|
|
15
|
+
return 'builtin';
|
|
16
|
+
}
|
|
17
|
+
const coreModulePattern = findCoreModule(name, settings);
|
|
18
|
+
if (coreModulePattern) {
|
|
19
|
+
return `core:${coreModulePattern}`;
|
|
20
|
+
}
|
|
21
|
+
if (isRelativeToParent(name)) {
|
|
22
|
+
return 'relative';
|
|
23
|
+
}
|
|
24
|
+
if (isIndex(name)) {
|
|
25
|
+
return 'index';
|
|
26
|
+
}
|
|
27
|
+
if (isRelativeToSibling(name)) {
|
|
28
|
+
return 'sibling';
|
|
29
|
+
}
|
|
30
|
+
if (isExternalLookingName(name)) {
|
|
31
|
+
return 'external';
|
|
32
|
+
}
|
|
33
|
+
return 'unknown';
|
|
34
|
+
}
|
|
35
|
+
function isInternalRegexMatch(name, settings) {
|
|
36
|
+
const internalScope = settings['mrpalmer/internalRegex'];
|
|
37
|
+
return internalScope && new RegExp(internalScope).test(name);
|
|
38
|
+
}
|
|
39
|
+
function isAbsolute(name) {
|
|
40
|
+
return typeof name === 'string' && path.isAbsolute(name);
|
|
41
|
+
}
|
|
42
|
+
function isBuiltIn(name) {
|
|
43
|
+
const base = baseModule(name);
|
|
44
|
+
return isBuiltin(base);
|
|
45
|
+
}
|
|
46
|
+
function findCoreModule(name, settings) {
|
|
47
|
+
const base = baseModule(name);
|
|
48
|
+
const coreModules = settings['mrpalmer/coreModules'] ?? [];
|
|
49
|
+
return coreModules.find((pattern) => minimatch(base, pattern, { nocomment: true }));
|
|
50
|
+
}
|
|
51
|
+
function baseModule(name) {
|
|
52
|
+
if (isScoped(name)) {
|
|
53
|
+
const [scope, pkg] = name.split('/');
|
|
54
|
+
return `${scope}/${pkg}`;
|
|
55
|
+
}
|
|
56
|
+
const [pkg] = name.split('/');
|
|
57
|
+
return pkg;
|
|
58
|
+
}
|
|
59
|
+
const scopedRegExp = /^@([^/]+)\/([^/]+)/;
|
|
60
|
+
function isScoped(name) {
|
|
61
|
+
return !!name && scopedRegExp.test(name);
|
|
62
|
+
}
|
|
63
|
+
function isRelativeToParent(name) {
|
|
64
|
+
return /^\.\.\/?/.test(name);
|
|
65
|
+
}
|
|
66
|
+
const indexFiles = new Set(['.', './', './index', './index.js']);
|
|
67
|
+
function isIndex(name) {
|
|
68
|
+
return indexFiles.has(name);
|
|
69
|
+
}
|
|
70
|
+
function isRelativeToSibling(name) {
|
|
71
|
+
return name.startsWith('./');
|
|
72
|
+
}
|
|
73
|
+
const packageNameSpecialCharsRegExp = /[~'!()*]/;
|
|
74
|
+
function isExternalLookingName(name) {
|
|
75
|
+
if (!name || name.trim() !== name || /^[._-]/.test(name) || /\s/.test(name)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const match = scopedRegExp.exec(name);
|
|
79
|
+
if (match) {
|
|
80
|
+
const [, user, pkg] = match;
|
|
81
|
+
return encodeURIComponent(user) === user && isExternalLookingName(pkg);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const [pkg] = name.split('/');
|
|
85
|
+
return (!packageNameSpecialCharsRegExp.test(pkg) &&
|
|
86
|
+
encodeURIComponent(pkg) === pkg);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function getSettings(context) {
|
|
2
|
+
const settings = context.settings;
|
|
3
|
+
return {
|
|
4
|
+
'mrpalmer/tsconfig': normalizeSetting(settings['mrpalmer/tsconfig'], isString),
|
|
5
|
+
'mrpalmer/coreModules': normalizeSetting(settings['mrpalmer/coreModules'], isArrayOfStrings),
|
|
6
|
+
'mrpalmer/internalRegex': normalizeSetting(settings['mrpalmer/internalRegex'], isString),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function normalizeSetting(setting, test) {
|
|
10
|
+
if (setting == null) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return test(setting) ? setting : undefined;
|
|
14
|
+
}
|
|
15
|
+
function isString(value) {
|
|
16
|
+
return typeof value === 'string';
|
|
17
|
+
}
|
|
18
|
+
function isArrayOfStrings(value) {
|
|
19
|
+
return Array.isArray(value) && value.every(isString);
|
|
20
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function getShortestPath(paths, options) {
|
|
2
|
+
// if there's only one path, return it
|
|
3
|
+
if (paths.size === 1) {
|
|
4
|
+
return paths.values().next().value;
|
|
5
|
+
}
|
|
6
|
+
const { breakTies } = options[0];
|
|
7
|
+
const sortedPaths = [...paths].sort(bySegmentCount);
|
|
8
|
+
const shortestLength = countSegments(sortedPaths[0]);
|
|
9
|
+
const ties = sortedPaths
|
|
10
|
+
.filter((path) => countSegments(path) === shortestLength)
|
|
11
|
+
.toSorted(byCharacterCount);
|
|
12
|
+
if (ties.length === 1) {
|
|
13
|
+
return ties[0];
|
|
14
|
+
}
|
|
15
|
+
const candidates = ties.filter(breakTies === 'alias' ? isAlias : isRelative);
|
|
16
|
+
if (candidates.length === 0) {
|
|
17
|
+
// if there are no candidates, it means there are multiple 'shortest' paths
|
|
18
|
+
// but none of them match the user's preference. fall back to the shortest
|
|
19
|
+
// by character count
|
|
20
|
+
return ties[0];
|
|
21
|
+
}
|
|
22
|
+
// at this point, there's either exactly one choice, or multiple choices that
|
|
23
|
+
// match the user's preference. just pick the shortest by character count
|
|
24
|
+
// (which is the first one in the array because the ties were already sorted)
|
|
25
|
+
return candidates[0];
|
|
26
|
+
}
|
|
27
|
+
function bySegmentCount(a, b) {
|
|
28
|
+
const aSegments = countSegments(a);
|
|
29
|
+
const bSegments = countSegments(b);
|
|
30
|
+
return aSegments - bSegments;
|
|
31
|
+
}
|
|
32
|
+
function byCharacterCount(a, b) {
|
|
33
|
+
const aLength = a.length;
|
|
34
|
+
const bLength = b.length;
|
|
35
|
+
return aLength - bLength;
|
|
36
|
+
}
|
|
37
|
+
function countSegments(path) {
|
|
38
|
+
return path.split('/').length;
|
|
39
|
+
}
|
|
40
|
+
function isRelative(path) {
|
|
41
|
+
return /^\.{1,2}\//.test(path);
|
|
42
|
+
}
|
|
43
|
+
function isAlias(path) {
|
|
44
|
+
return !isRelative(path);
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createPathsMatcher } from 'get-tsconfig';
|
|
2
|
+
import type { PluginSettings } from '../types.js';
|
|
3
|
+
export { createPathsMatcher };
|
|
4
|
+
export declare function loadConfig(settings: PluginSettings): {
|
|
5
|
+
path: string;
|
|
6
|
+
config: {
|
|
7
|
+
compilerOptions?: import("get-tsconfig").TsConfigJson.CompilerOptions | undefined;
|
|
8
|
+
watchOptions?: import("get-tsconfig").TsConfigJson.WatchOptions | undefined;
|
|
9
|
+
typeAcquisition?: import("get-tsconfig").TsConfigJson.TypeAcquisition | undefined;
|
|
10
|
+
compileOnSave?: boolean | undefined;
|
|
11
|
+
files?: string[] | undefined;
|
|
12
|
+
exclude?: string[] | undefined;
|
|
13
|
+
include?: string[] | undefined;
|
|
14
|
+
references?: import("get-tsconfig").TsConfigJson.References[] | undefined;
|
|
15
|
+
};
|
|
16
|
+
} | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createPathsMatcher, parseTsconfig } from 'get-tsconfig';
|
|
4
|
+
export { createPathsMatcher };
|
|
5
|
+
export function loadConfig(settings) {
|
|
6
|
+
const eslintConfigPath = findUp([
|
|
7
|
+
'eslint.config.js',
|
|
8
|
+
'eslint.config.mjs',
|
|
9
|
+
'eslint.config.cjs',
|
|
10
|
+
]);
|
|
11
|
+
if (!eslintConfigPath || !fs.existsSync(eslintConfigPath)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const configFilePath = path.resolve(path.dirname(eslintConfigPath), getRelativeConfigFilePath(settings));
|
|
15
|
+
if (!fs.existsSync(configFilePath)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const config = parseTsconfig(configFilePath);
|
|
19
|
+
return { path: configFilePath, config };
|
|
20
|
+
}
|
|
21
|
+
function getRelativeConfigFilePath(settings) {
|
|
22
|
+
return settings['mrpalmer/tsconfig'] ?? 'tsconfig.json';
|
|
23
|
+
}
|
|
24
|
+
function findUp(filenames) {
|
|
25
|
+
let directory = process.cwd();
|
|
26
|
+
const { root: stopAt } = path.parse(directory);
|
|
27
|
+
while (directory !== stopAt) {
|
|
28
|
+
const found = firstExistingPath(filenames, directory);
|
|
29
|
+
if (found) {
|
|
30
|
+
return path.resolve(directory, found);
|
|
31
|
+
}
|
|
32
|
+
directory = path.dirname(directory);
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
function firstExistingPath(paths, directory) {
|
|
37
|
+
for (const p of paths) {
|
|
38
|
+
try {
|
|
39
|
+
const stat = fs.statSync(path.resolve(directory, p), {
|
|
40
|
+
throwIfNoEntry: false,
|
|
41
|
+
});
|
|
42
|
+
if (!stat) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (stat.isFile()) {
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// do nothing
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mrpalmer/eslint-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Custom ESLint rules for Mike Palmer's projects",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./lib/index.d.ts",
|
|
10
|
+
"default": "./lib/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"main": "./lib/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"lib"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@typescript-eslint/utils": "^8.29.1",
|
|
20
|
+
"get-tsconfig": "^4.10.0",
|
|
21
|
+
"minimatch": "^10.0.1"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"eslint": "^9.0.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20",
|
|
28
|
+
"npm": ">=8",
|
|
29
|
+
"yarn": ">=1"
|
|
30
|
+
}
|
|
31
|
+
}
|