Initial commit

This commit is contained in:
Alex
2023-07-19 23:40:39 -04:00
commit fd67e90cbc
24 changed files with 7998 additions and 0 deletions

156
src/babel.ts Normal file
View File

@ -0,0 +1,156 @@
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>[];
}
}

View File

@ -0,0 +1,39 @@
import type { Rule } from "eslint";
const rule: Rule.RuleModule = {
meta: {
type: "problem",
docs: {
description:
"Bans import from the specifier '.' and '..' and replaces it with '.+/index'",
category: "Best Practices",
recommended: true,
},
fixable: "code",
},
create: context => ({
ImportDeclaration(node) {
if (node.source.value === ".") {
context.report({
node: node.source,
message:
"Importing from the specifier '.' is not allowed. Use './index' instead.",
fix(fixer) {
return fixer.replaceText(node.source, '"./index"');
},
});
} else if (node.source.value === "..") {
context.report({
node: node.source,
message:
"Importing from the specifier '..' is not allowed. Use '../index' instead.",
fix(fixer) {
return fixer.replaceText(node.source, '"../index"');
},
});
}
},
}),
};
export default rule;

View File

@ -0,0 +1,32 @@
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

@ -0,0 +1,33 @@
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;

View File

@ -0,0 +1,36 @@
// @ts-check
import { builtinModules } from "node:module";
import type { Rule } from "eslint";
const rule: Rule.RuleModule = {
meta: {
type: "problem",
docs: {
description:
"Disallow imports of built-in Node.js modules without the `node:` prefix",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
},
create: context => ({
ImportDeclaration(node) {
const { source } = node;
if (source?.type === "Literal" && typeof source.value === "string") {
const moduleName = source.value;
if (builtinModules.includes(moduleName) && !moduleName.startsWith("node:")) {
context.report({
node: source,
message: `Import of built-in Node.js module "${moduleName}" must use the "node:" prefix.`,
fix: fixer => fixer.replaceText(source, `"node:${moduleName}"`),
});
}
}
},
}),
};
export default rule;