@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.
@@ -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,3 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ import type { PluginSettings } from '../types.js';
3
+ export declare function getSettings(context: TSESLint.RuleContext<string, unknown[]>): PluginSettings;
@@ -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,2 @@
1
+ import type { Options as ShortenPathRuleOptions } from '../rules/shorten-paths.js';
2
+ export declare function getShortestPath(paths: Set<string>, options: Readonly<ShortenPathRuleOptions>): string | undefined;
@@ -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
+ }