Initial commit
This commit is contained in:
156
src/babel.ts
Normal file
156
src/babel.ts
Normal 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>[];
|
||||
}
|
||||
}
|
39
src/rules/no-import-dot.ts
Normal file
39
src/rules/no-import-dot.ts
Normal 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;
|
32
src/rules/no-new-prisma.ts
Normal file
32
src/rules/no-new-prisma.ts
Normal 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;
|
33
src/rules/no-webcrypto-import.ts
Normal file
33
src/rules/no-webcrypto-import.ts
Normal 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;
|
36
src/rules/require-node-prefix.ts
Normal file
36
src/rules/require-node-prefix.ts
Normal 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;
|
Reference in New Issue
Block a user