Update
This commit is contained in:
@ -7,14 +7,18 @@ const files = readdirSync('./src/rules')
|
||||
.filter(file => file !== 'index.ts')
|
||||
.map(file => file.slice(0, -3));
|
||||
|
||||
const entryFile = `
|
||||
const entryFile = /* js */ `
|
||||
import type { Rule } from 'eslint';
|
||||
import type { ESLintUtils } from '@typescript-eslint/utils';
|
||||
|
||||
${files.map(file => `import ${camelCase(file)} from "./${file}"`).join(';\n')}
|
||||
${files.map(file => `import ${camelCase(file)} from './${file}';`).join('\n')}
|
||||
|
||||
export const rules: Record<string, Rule.RuleModule> = {
|
||||
${files.map(file => `"${file}": ${camelCase(file)}`).join(',\n ')}
|
||||
export const rules: Record<
|
||||
string,
|
||||
Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>
|
||||
> = {
|
||||
${files.map(file => `'${file}': ${camelCase(file)},`).join('\n ')}
|
||||
};
|
||||
`.trim();
|
||||
|
||||
writeFileSync('./src/rules/index.ts', entryFile);
|
||||
writeFileSync('./src/rules/index.ts', entryFile + '\n');
|
||||
|
42
src/index.ts
42
src/index.ts
@ -1,9 +1,10 @@
|
||||
import './redirect';
|
||||
import type { ESLintConfig } from 'eslint-define-config';
|
||||
import type { ESLintConfig, Rules } from 'eslint-define-config';
|
||||
import { typescriptRules } from './presets/typescript';
|
||||
import { unicornRules } from './presets/unicorn';
|
||||
import { eslintRules } from './presets/eslint';
|
||||
import { reactRules } from './presets/react';
|
||||
import { importRules } from './presets/import';
|
||||
import { error, warn, off } from './constants';
|
||||
// @ts-expect-error
|
||||
const { name } = (0, require)('./package.json');
|
||||
@ -14,12 +15,39 @@ const unique = <T>(arr: T[]): T[] => [...new Set(arr)];
|
||||
const ensureArray = <T>(value?: T | T[]): T[] =>
|
||||
value == null ? [] : Array.isArray(value) ? value : [value];
|
||||
|
||||
type RuleLevel = 'error' | 'warn' | 'off' | 0 | 1 | 2;
|
||||
type RuleEntry<Options> = RuleLevel | [RuleLevel, Partial<Options>];
|
||||
|
||||
declare module 'eslint-define-config/src/rules/react/no-unknown-property.d.ts' {
|
||||
export interface NoUnknownPropertyOption {
|
||||
extends: ('next' | 'emotion')[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface LocalRuleOptions {
|
||||
/** Bans import from the specifier '.' and '..' and replaces it with '.+/index' */
|
||||
'rules/no-import-dot': RuleEntry<unknown>;
|
||||
/**
|
||||
* Enforce template literal expressions to be of `string` type
|
||||
* @see [restrict-template-expressions](https://typescript-eslint.io/rules/restrict-template-expressions)
|
||||
*/
|
||||
'rules/restrict-template-expressions': RuleEntry<{ allow: string[] }>;
|
||||
}
|
||||
|
||||
export type RuleOptions = Rules & Partial<LocalRuleOptions>;
|
||||
|
||||
/**
|
||||
* ESLint Configuration.
|
||||
* @see [ESLint Configuration](https://eslint.org/docs/latest/user-guide/configuring/)
|
||||
*/
|
||||
type Config = Omit<ESLintConfig, 'rules'> & {
|
||||
/**
|
||||
* Rules.
|
||||
* @see [Rules](https://eslint.org/docs/latest/user-guide/configuring/rules)
|
||||
*/
|
||||
rules?: RuleOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a ESLint config object.
|
||||
*
|
||||
@ -33,16 +61,16 @@ export function extendConfig({
|
||||
extends: _extends,
|
||||
overrides,
|
||||
...rest
|
||||
}: ESLintConfig = {}): ESLintConfig {
|
||||
}: Config = {}): ESLintConfig {
|
||||
const hasReact = plugins?.includes('react');
|
||||
const hasReactRefresh = plugins?.includes('react-refresh');
|
||||
const hasUnicorn = plugins?.includes('unicorn');
|
||||
const hasNext = ensureArray(_extends).some(name => name.includes(':@next/next'));
|
||||
|
||||
const result: ESLintConfig = {
|
||||
const result: Config = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: unique(['@typescript-eslint', 'import', ...(plugins ?? [])]),
|
||||
plugins: unique(['@typescript-eslint', 'import', 'rules', ...(plugins ?? [])]),
|
||||
env: { node: true, browser: true, es2023: true },
|
||||
reportUnusedDisableDirectives: true,
|
||||
parserOptions: {
|
||||
@ -99,9 +127,7 @@ export function extendConfig({
|
||||
rules: {
|
||||
...eslintRules,
|
||||
...typescriptRules,
|
||||
'import/export': off,
|
||||
'import/no-duplicates': error,
|
||||
'import/order': [error, { groups: ['builtin', 'external'] }],
|
||||
...importRules,
|
||||
...(hasReact && {
|
||||
...reactRules,
|
||||
'react/no-unknown-property': [
|
||||
@ -113,6 +139,8 @@ export function extendConfig({
|
||||
'react-refresh/only-export-components': [warn, { allowConstantExport: true }],
|
||||
}),
|
||||
...(hasUnicorn && unicornRules),
|
||||
'rules/no-import-dot': error,
|
||||
'rules/restrict-template-expressions': error,
|
||||
...rules,
|
||||
},
|
||||
...rest,
|
||||
|
8
src/presets/import.ts
Normal file
8
src/presets/import.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { error, off } from '../constants';
|
||||
import { ImportRules } from 'eslint-define-config/src/rules/import';
|
||||
|
||||
export const importRules: Partial<ImportRules> = {
|
||||
'import/export': off,
|
||||
'import/no-duplicates': error,
|
||||
'import/order': [error, { groups: ['builtin', 'external'] }],
|
||||
};
|
6
src/presets/local.ts
Normal file
6
src/presets/local.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { error, off } from '../constants';
|
||||
import type { CustomRuleOptions } from 'eslint-define-config';
|
||||
|
||||
export const importRules: Partial<CustomRuleOptions> = {
|
||||
'local/no-import-dot': error,
|
||||
};
|
@ -33,6 +33,7 @@ export const typescriptRules: Partial<TypeScriptRules> = {
|
||||
],
|
||||
'@typescript-eslint/no-use-before-define': off,
|
||||
'@typescript-eslint/no-var-requires': off,
|
||||
'@typescript-eslint/restrict-template-expressions': off,
|
||||
'@typescript-eslint/triple-slash-reference': off,
|
||||
'@typescript-eslint/unbound-method': off,
|
||||
};
|
||||
|
@ -1,7 +1,13 @@
|
||||
import type { Rule } from 'eslint';
|
||||
import type { ESLintUtils } from '@typescript-eslint/utils';
|
||||
|
||||
import noImportDot from "./no-import-dot"
|
||||
import noImportDot from './no-import-dot';
|
||||
import restrictTemplateExpressions from './restrict-template-expressions';
|
||||
|
||||
export const rules: Record<string, Rule.RuleModule> = {
|
||||
"no-import-dot": noImportDot
|
||||
};
|
||||
export const rules: Record<
|
||||
string,
|
||||
Rule.RuleModule | ESLintUtils.RuleModule<string, unknown[]>
|
||||
> = {
|
||||
'no-import-dot': noImportDot,
|
||||
'restrict-template-expressions': restrictTemplateExpressions,
|
||||
};
|
||||
|
@ -1,19 +1,19 @@
|
||||
import type { Rule } from "eslint";
|
||||
import type { Rule } from 'eslint';
|
||||
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
"Bans import from the specifier '.' and '..' and replaces it with '.+/index'",
|
||||
category: "Best Practices",
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: "code",
|
||||
fixable: 'code',
|
||||
},
|
||||
create: context => ({
|
||||
ImportDeclaration(node) {
|
||||
if (node.source.value === ".") {
|
||||
if (node.source.value === '.') {
|
||||
context.report({
|
||||
node: node.source,
|
||||
message:
|
||||
@ -22,7 +22,7 @@ const rule: Rule.RuleModule = {
|
||||
return fixer.replaceText(node.source, '"./index"');
|
||||
},
|
||||
});
|
||||
} else if (node.source.value === "..") {
|
||||
} else if (node.source.value === '..') {
|
||||
context.report({
|
||||
node: node.source,
|
||||
message:
|
||||
|
122
src/rules/restrict-template-expressions.ts
Normal file
122
src/rules/restrict-template-expressions.ts
Normal file
@ -0,0 +1,122 @@
|
||||
// https://github.com/typescript-eslint/typescript-eslint/blob/75c128856b1ce05a4fec799bfa6de03b3dab03d0/packages/eslint-plugin/src/rules/restrict-template-expressions.ts
|
||||
import * as ts from 'typescript';
|
||||
import { ESLintUtils, type TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
|
||||
import {
|
||||
getTypeName,
|
||||
isTypeAnyType,
|
||||
isTypeFlagSet,
|
||||
isTypeNeverType,
|
||||
getConstrainedTypeAtLocation,
|
||||
} from '@typescript-eslint/type-utils';
|
||||
import { getParserServices } from '@typescript-eslint/utils/eslint-utils';
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator(
|
||||
name => `https://typescript-eslint.io/rules/${name}`,
|
||||
);
|
||||
|
||||
interface Option {
|
||||
allow: string[];
|
||||
}
|
||||
const defaultOption: Option = {
|
||||
allow: ['any', 'boolean', 'null', 'undefined', 'number', 'RegExp', 'URLSearchParams'],
|
||||
};
|
||||
|
||||
type MessageId = 'invalidType';
|
||||
|
||||
export default createRule<Option[], MessageId>({
|
||||
name: 'restrict-template-expressions',
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce template literal expressions to be of `string` type',
|
||||
recommended: 'recommended',
|
||||
requiresTypeChecking: true,
|
||||
},
|
||||
messages: {
|
||||
invalidType: 'Invalid type "{{type}}" of template literal expression.',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
allow: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
description: 'Allow specific types',
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultOptions: [defaultOption],
|
||||
create(context, [options]) {
|
||||
const services = getParserServices(context);
|
||||
const checker = services.program!.getTypeChecker();
|
||||
const allowed = new Set(options.allow);
|
||||
|
||||
const { StringLike, NumberLike, BigIntLike, BooleanLike, Null, Undefined } =
|
||||
ts.TypeFlags;
|
||||
|
||||
function isUnderlyingTypePrimitive(type: ts.Type): boolean {
|
||||
return (
|
||||
isTypeFlagSet(type, StringLike) ||
|
||||
(allowed.has('number') && isTypeFlagSet(type, NumberLike | BigIntLike)) ||
|
||||
(allowed.has('boolean') && isTypeFlagSet(type, BooleanLike)) ||
|
||||
(allowed.has('any') && isTypeAnyType(type)) ||
|
||||
allowed.has(getTypeName(checker, type)) ||
|
||||
(allowed.has('null') && isTypeFlagSet(type, Null)) ||
|
||||
(allowed.has('undefined') && isTypeFlagSet(type, Undefined)) ||
|
||||
(allowed.has('never') && isTypeNeverType(type))
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
TemplateLiteral(node: TSESTree.TemplateLiteral): void {
|
||||
// don't check tagged template literals
|
||||
if (node.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const expression of node.expressions) {
|
||||
const expressionType = getConstrainedTypeAtLocation(services, expression);
|
||||
|
||||
if (
|
||||
!isInnerUnionOrIntersectionConformingTo(
|
||||
expressionType,
|
||||
isUnderlyingTypePrimitive,
|
||||
)
|
||||
) {
|
||||
context.report({
|
||||
node: expression,
|
||||
messageId: 'invalidType',
|
||||
data: { type: checker.typeToString(expressionType) },
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function isInnerUnionOrIntersectionConformingTo(
|
||||
type: ts.Type,
|
||||
predicate: (underlyingType: ts.Type) => boolean,
|
||||
): boolean {
|
||||
return rec(type);
|
||||
|
||||
function rec(innerType: ts.Type): boolean {
|
||||
if (innerType.isUnion()) {
|
||||
return innerType.types.every(rec);
|
||||
}
|
||||
|
||||
if (innerType.isIntersection()) {
|
||||
return innerType.types.some(rec);
|
||||
}
|
||||
|
||||
return predicate(innerType);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
15
src/types.d.ts
vendored
Normal file
15
src/types.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
declare module '@typescript-eslint/utils' {
|
||||
export * from '@typescript-eslint/utils/dist/index';
|
||||
}
|
||||
declare module '@typescript-eslint/typescript-estree' {
|
||||
export * from '@typescript-eslint/typescript-estree/dist/index';
|
||||
}
|
||||
declare module '@typescript-eslint/type-utils' {
|
||||
export * from '@typescript-eslint/type-utils/dist/index';
|
||||
}
|
||||
declare module '@typescript-eslint/utils/eslint-utils' {
|
||||
export * from '@typescript-eslint/utils/dist/eslint-utils';
|
||||
}
|
||||
declare module '@typescript-eslint/utils/json-schema' {
|
||||
export * from '@typescript-eslint/utils/dist/json-schema';
|
||||
}
|
Reference in New Issue
Block a user