This commit is contained in:
Alex
2023-07-22 01:00:28 -04:00
parent 77444efbf2
commit 3debdb9e74
27 changed files with 1756 additions and 585 deletions

20
src/addAlias.ts Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
import fs from 'node:fs';
import { resolve } from 'node:path';
import { name } from '../dist/package.json';
const pkgPath = resolve(process.cwd(), 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
pkg.devDependencies ??= {};
Object.assign(pkg.devDependencies, {
'eslint-plugin-import': `file:./node_modules/${name}/import`,
'eslint-plugin-jsx-a11y': `file:./node_modules/${name}/jsx-a11y`,
'eslint-plugin-local': `file:./node_modules/${name}/local`,
'eslint-plugin-rules': `file:./node_modules/${name}/rules`,
'eslint-plugin-react': `file:./node_modules/${name}/react`,
'eslint-plugin-react-hooks': `file:./node_modules/${name}/react-hooks`,
});
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));

View File

@ -1,156 +0,0 @@
import assert from 'node:assert';
import { readFileSync } from 'node:fs';
import { extname } from 'node:path';
import * as babel from '@babel/core';
import type { types as t } from '@babel/core';
import type { Loader, Plugin } from 'esbuild';
import { createMacro, type MacroHandler } from 'babel-plugin-macros';
class HandlerMap {
map = new Map<string, MacroHandler>();
set(names: string | string[], handler: MacroHandler) {
names = Array.isArray(names) ? names : [names];
const macro = createMacro(handler);
for (const name of names) {
this.map.set(name, macro);
}
return this;
}
get keys() {
return Array.from(this.map.keys());
}
resolvePath = (module: string) => module;
require = (module: string) => this.map.get(module);
isMacrosName = (module: string) => this.map.has(module);
}
const map = new HandlerMap()
.set(
'object.assign',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('assign'))),
)
.set(
['object-values', 'object.values'],
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('values'))),
)
.set(
'object.fromentries',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('fromEntries'))),
)
.set(
'object.entries',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('entries'))),
)
.set(
'has',
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
)
.set(
'array-includes',
proto(t => t.identifier('includes')),
)
.set(
'array.prototype.flatmap',
proto(t => t.identifier('flatMap')),
)
.set(
'array.prototype.flat',
proto(t => t.identifier('flat')),
)
.set(
'array.prototype.findlastindex',
proto(t => t.identifier('findLastIndex')),
)
.set(
'array.prototype.tosorted',
proto(t => t.identifier('toSorted')),
)
.set(
'string.prototype.matchall',
proto(t => t.identifier('matchAll')),
);
function replace(getReplacement: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => {
references.default.forEach(referencePath => {
referencePath.replaceWith(getReplacement(t));
});
};
}
function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
return ({ references, babel: { types: t } }) => {
references.default.forEach(referencePath => {
const { parent, parentPath } = referencePath;
assert(t.isCallExpression(parent));
const [callee, ...rest] = parent.arguments;
parentPath!.replaceWith(
t.callExpression(
t.memberExpression(callee as t.Expression, getProperty(t)),
rest,
),
);
});
};
}
export const babelPlugin: Plugin = {
name: 'babel',
setup(build) {
const { keys, ...macroOptions } = map;
build.onLoad({ filter: /\.[jt]sx?$/ }, args => {
const { path } = args;
if (path.includes('node_modules/')) {
return null;
}
let source = readFileSync(path, 'utf-8')
.replaceAll("require('object.hasown/polyfill')()", 'Object.hasOwn')
.replaceAll("require('object.fromentries/polyfill')()", 'Object.fromEntries');
if (
path.includes('eslint-plugin-import/src/rules/') ||
path.includes('eslint-plugin-import/config/')
) {
source = source.replace('\nmodule.exports = {', '\nexport default {');
}
const isFlow = source.includes('@flow');
const loader = extname(path).slice(1) as Loader;
if (!isFlow && !keys.some(key => source.includes(key))) {
return { contents: source, loader };
}
const res = babel.transformSync(source, {
filename: path,
babelrc: false,
configFile: false,
parserOpts: {
plugins: [isFlow ? 'flow' : 'typescript'],
},
plugins: [
isFlow && '@babel/plugin-transform-flow-strip-types',
['babel-plugin-macros', macroOptions],
].filter(Boolean),
})!;
return {
contents: res.code!,
loader,
};
});
},
};
declare global {
interface Array<T> {
filter(
predicate: BooleanConstructor,
): Exclude<T, null | undefined | false | '' | 0>[];
}
}

131
src/basic.ts Normal file
View File

@ -0,0 +1,131 @@
// @ts-check
import type { ESLintConfig } from 'eslint-define-config';
export function extendConfig({
plugins,
settings,
rules,
...config
}: ESLintConfig): ESLintConfig {
return {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'import', ...(plugins ?? [])],
env: { node: true, browser: true },
reportUnusedDisableDirectives: true,
parserOptions: { project: ['./tsconfig.json'] },
extends: [
'eslint:recommended',
'prettier',
'plugin:@typescript-eslint/recommended',
'plugin:import/errors',
'plugin:import/typescript',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
],
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/core-modules': ['node:test'],
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json',
},
},
react: {
version: 'detect',
},
...settings,
},
rules: {
'no-duplicate-imports': 'error',
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'crypto',
importNames: ['webcrypto'],
message: 'Use global `crypto` instead',
},
],
},
],
'no-restricted-globals': ['error', 'event', 'name'],
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/consistent-type-imports': [
'error',
{ disallowTypeAnnotations: false, fixStyle: 'inline-type-imports' },
],
'@typescript-eslint/ban-types': [
'error',
{
extendDefaults: false,
types: {
String: { message: 'Use string instead', fixWith: 'string' },
Number: { message: 'Use number instead', fixWith: 'number' },
Boolean: { message: 'Use boolean instead', fixWith: 'boolean' },
Symbol: { message: 'Use symbol instead', fixWith: 'symbol' },
},
},
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/triple-slash-reference': 'off',
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{ ignoreRestSiblings: true, varsIgnorePattern: '^_' },
],
'arrow-body-style': ['error', 'as-needed'],
'class-methods-use-this': 'off',
complexity: ['warn', { max: 100 }],
curly: ['error', 'multi-line', 'consistent'],
eqeqeq: ['error', 'smart'],
'no-async-promise-executor': 'off',
'no-case-declarations': 'off',
'no-console': 'warn',
'no-constant-condition': ['error', { checkLoops: false }],
'no-debugger': 'off',
'no-empty': ['error', { allowEmptyCatch: true }],
'no-inner-declarations': 'off',
'no-lonely-if': 'error',
'no-template-curly-in-string': 'error',
'no-var': 'error',
'import/export': 'off',
'import/order': ['error', { groups: ['builtin', 'external'] }],
'object-shorthand': ['error', 'always', { ignoreConstructors: true }],
'one-var': ['error', { var: 'never', let: 'never' }],
'prefer-arrow-callback': 'error',
'prefer-const': ['error', { destructuring: 'all' }],
'prefer-destructuring': [
'warn',
{ AssignmentExpression: { array: false, object: false } },
],
'prefer-object-spread': 'error',
'prefer-rest-params': 'warn',
'prefer-spread': 'warn',
'quote-props': ['error', 'as-needed'],
'spaced-comment': ['error', 'always', { markers: ['/'] }],
'sort-imports': ['warn', { ignoreDeclarationSort: true }],
yoda: ['error', 'never', { exceptRange: true }],
'react/display-name': 'off',
'react/no-children-prop': 'error',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'react/no-unknown-property': ['error', { ignore: ['css'] }],
...rules,
},
...config,
};
}

20
src/build-local-rules.ts Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env -S node -r esbin
import { readdirSync, writeFileSync } from 'node:fs';
import { camelCase } from 'lodash';
const files = readdirSync('./src/rules')
.filter(file => file.endsWith('.ts'))
.filter(file => file !== 'index.ts')
.map(file => file.slice(0, -3));
const entryFile = `
import type { Rule } from 'eslint';
${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 ')}
};
`.trim();
writeFileSync('./src/rules/index.ts', entryFile);

29
src/local/index.ts Normal file
View File

@ -0,0 +1,29 @@
import type { ESLint } from 'eslint';
import * as fs from 'node:fs';
import { resolve, basename, extname } from 'node:path';
function tryRequire(candidates: string[]) {
for (const candidate of candidates) {
try {
require(candidate);
return;
} catch {}
}
}
tryRequire(['esbin', 'esbuild-register', 'ts-node/register/transpile-only']);
const folders = resolve(process.cwd(), 'eslint-local-rules');
const files = fs.readdirSync(folders);
const plugin: ESLint.Plugin = {
rules: {},
};
for (const file of files) {
const name = basename(file, extname(file));
const module = require(resolve(folders, file));
plugin.rules![name] = module.default ?? module;
}
export = plugin;

9
src/rules/index.ts Normal file
View File

@ -0,0 +1,9 @@
import type { Rule } from 'eslint';
import noImportDot from "./no-import-dot";
import requireNodePrefix from "./require-node-prefix"
export const rules: Record<string, Rule.RuleModule> = {
"no-import-dot": noImportDot,
"require-node-prefix": requireNodePrefix
};

View File

@ -1,32 +0,0 @@
import type { Rule } from "eslint";
const rule: Rule.RuleModule = {
meta: {
type: "problem",
docs: {
description: "Disallow direct usage of `new PrismaClient()`",
category: "Best Practices",
recommended: true,
},
},
create(context) {
// Check if the file is the target file where the import is allowed
if (context.filename.endsWith("src/utils/db.ts")) {
return {};
}
return {
NewExpression(node) {
if (node.callee.type === "Identifier" && node.callee.name === "PrismaClient") {
context.report({
node,
message:
"Avoid direct usage of `new PrismaClient()`. Import from `src/utils/db.ts` instead.",
});
}
},
};
},
};
export default rule;

View File

@ -1,33 +0,0 @@
import type { Rule } from "eslint";
const rule: Rule.RuleModule = {
meta: {
type: "problem",
docs: {
description: "Disallow importing webcrypto from node:crypto and crypto modules",
category: "Best Practices",
recommended: true,
},
schema: [],
},
create: context => ({
ImportDeclaration(node) {
const importedSource = node.source.value as string;
const importedSpecifier = node.specifiers[0];
if (
(importedSource === "crypto" || importedSource === "node:crypto") &&
importedSpecifier.type === "ImportSpecifier" &&
importedSpecifier.local.name === "webcrypto"
) {
context.report({
node: importedSpecifier,
message:
"Do not import 'webcrypto' from 'crypto' or 'node:crypto'. Use the global variable 'crypto' instead.",
});
}
},
}),
};
export default rule;