253 lines
9.9 KiB
JavaScript
253 lines
9.9 KiB
JavaScript
import { FlatCompat } from '@eslint/eslintrc';
|
||
import { fileURLToPath } from 'node:url';
|
||
import path from 'node:path';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
const compat = new FlatCompat({
|
||
baseDirectory: __dirname,
|
||
});
|
||
|
||
const REMOTE_CSS_IMPORT_PATTERN =
|
||
/@import\s+(?:url\(\s*['"]?((?:https?:)?\/\/[^'")\s]+)['"]?\s*\)|['"]((?:https?:)?\/\/[^'"\s]+)['"])/g;
|
||
|
||
const cssImportGuardPlugin = {
|
||
processors: {
|
||
css: {
|
||
preprocess(text) {
|
||
const lines = text.split('\n');
|
||
const virtualLines = lines.map(line => {
|
||
const matches = [...line.matchAll(REMOTE_CSS_IMPORT_PATTERN)];
|
||
|
||
if (matches.length === 0) {
|
||
return '';
|
||
}
|
||
|
||
return matches
|
||
.map(match => {
|
||
const url = match[1] ?? match[2];
|
||
|
||
return `__cssExternalImport(${JSON.stringify(url)});`;
|
||
})
|
||
.join(' ');
|
||
});
|
||
|
||
return [virtualLines.join('\n')];
|
||
},
|
||
postprocess(messages) {
|
||
return messages.flat();
|
||
},
|
||
supportsAutofix: false,
|
||
},
|
||
},
|
||
};
|
||
|
||
const baseRestrictedSyntaxRules = [
|
||
{
|
||
selector: "MemberExpression[object.name='process'][property.name='env']",
|
||
message:
|
||
'工程规范:请勿在 src 目录下直接使用 process.env\n如需获取 URL 请求前缀,请使用已经注入全局的 PROJECT_DOMAIN',
|
||
},
|
||
{
|
||
selector:
|
||
":matches(ExportNamedDeclaration, ExportDefaultDeclaration) :matches([id.name='Network'], [declaration.id.name='Network'])",
|
||
message:
|
||
"工程规范:禁止自行定义 Network,项目已提供 src/network.ts,请直接使用: import { Network } from '@/network'",
|
||
},
|
||
{
|
||
selector:
|
||
'Literal[value=/(^|\\s)(?:[^\\s:]+:)*(bg|text|border|divide|outline|ring|ring-offset|from|to|via|decoration|shadow|accent|caret|fill|stroke)-[a-z0-9-]+\\/([0-9]+|\\[[^\\]]+\\])/], TemplateElement[value.raw=/(^|\\s)(?:[^\\s:]+:)*(bg|text|border|divide|outline|ring|ring-offset|from|to|via|decoration|shadow|accent|caret|fill|stroke)-[a-z0-9-]+\\/([0-9]+|\\[[^\\]]+\\])/]',
|
||
message:
|
||
'微信小程序兼容性:禁用 Tailwind 颜色不透明度简写(如 bg-primary/10),该语法在微信小程序下 opacity 会丢失。请拆分写(如 bg-primary bg-opacity-10)。',
|
||
},
|
||
{
|
||
selector:
|
||
'Literal[value=/(^|\\s)peer-[a-z0-9-]+\\b/], TemplateElement[value.raw=/(^|\\s)peer-[a-z0-9-]+\\b/]',
|
||
message:
|
||
'微信小程序兼容性:不支持 Tailwind 的 peer-*(如 peer-checked、peer-disabled)。',
|
||
},
|
||
{
|
||
selector:
|
||
'Literal[value=/(^|\\s)group-[a-z0-9-]+\\b/], TemplateElement[value.raw=/(^|\\s)group-[a-z0-9-]+\\b/]',
|
||
message: '微信小程序兼容性:不支持 Tailwind 的 group-*(如 group-hover)。',
|
||
},
|
||
{
|
||
selector:
|
||
'Literal[value=/\\b(?!gap(?:-x|-y)?-)[a-zA-Z0-9-]+\\-[0-9]+\\.[0-9]+\\b/], TemplateElement[value.raw=/\\b(?!gap(?:-x|-y)?-)[a-zA-Z0-9-]+\\-[0-9]+\\.[0-9]+\\b/]',
|
||
message:
|
||
'微信小程序兼容性:禁用 Tailwind 小数值类名(如 space-y-1.5、w-0.5),请用整数替代(如 space-y-2、w-1)。',
|
||
},
|
||
{
|
||
selector:
|
||
":matches(JSXAttribute[name.name='className'], CallExpression[callee.name=/^(cn|cva)$/]) :matches(Literal[value=/\\:has\\(/], TemplateElement[value.raw=/\\:has\\(/])",
|
||
message: '微信小程序兼容性:WXSS 不支持 :has(...)(会导致预览上传失败)。',
|
||
},
|
||
{
|
||
selector:
|
||
":matches(JSXAttribute[name.name='className'], CallExpression[callee.name=/^(cn|cva)$/]) :matches(Literal[value=/(^|\\s)has-[^\\s]+/], TemplateElement[value.raw=/(^|\\s)has-[^\\s]+/])",
|
||
message:
|
||
'微信小程序兼容性:禁用 Tailwind 的 has-* 变体(会生成 :has,导致预览上传失败)。',
|
||
},
|
||
{
|
||
selector:
|
||
":matches(JSXAttribute[name.name='className'], CallExpression[callee.name=/^(cn|cva)$/]) :matches(Literal[value=/\\[&>\\*/], TemplateElement[value.raw=/\\[&>\\*/])",
|
||
message:
|
||
'微信小程序兼容性:禁用 [&>*...](可能生成非法 WXSS,如 >:last-child)。请改为 [&>view] 等明确标签。',
|
||
},
|
||
{
|
||
selector:
|
||
":matches(JSXAttribute[name.name='className'], CallExpression[callee.name=/^(cn|cva)$/]) :matches(Literal[value=/\\[&[^\\]]*\\[data-/], TemplateElement[value.raw=/\\[&[^\\]]*\\[data-/])",
|
||
message:
|
||
'微信小程序兼容性:禁用 Tailwind 任意选择器里的属性选择器(如 [&>[data-...]]),可能导致预览上传失败。',
|
||
},
|
||
{
|
||
selector:
|
||
":matches(JSXAttribute[name.name='className'], CallExpression[callee.name=/^(cn|cva)$/]) :matches(Literal[value=/\\[[^\\]]*&[^\\]]*~[^\\]]*\\]/], TemplateElement[value.raw=/\\[[^\\]]*&[^\\]]*~[^\\]]*\\]/])",
|
||
message: '微信小程序兼容性:WXSS 不支持 ~(会导致预览上传失败)。',
|
||
},
|
||
{
|
||
selector:
|
||
"CallExpression[callee.name='__cssExternalImport'] > Literal[value=/^(?:https?:)?\\/\\//]",
|
||
message:
|
||
'微信小程序兼容性:禁止在 CSS/WXSS 中使用远程 @import(如 Google Fonts)。请改为本地静态资源或删除该导入。',
|
||
},
|
||
{
|
||
selector:
|
||
"JSXAttribute[name.name='color'][value.type='Literal'][value.value='currentColor'], JSXAttribute[name.name='color'] > JSXExpressionContainer > Literal[value='currentColor']",
|
||
message:
|
||
'lucide-react-taro 规范:禁止使用 color="currentColor",小程序端不会按预期继承颜色。请改为显式颜色值,或通过 LucideTaroProvider 提供默认颜色。',
|
||
},
|
||
];
|
||
|
||
const pageRestrictedSyntaxRules = [
|
||
{
|
||
selector:
|
||
"ImportDeclaration[source.value='@tarojs/components'] ImportSpecifier[imported.name='Button']",
|
||
message:
|
||
"组件规范:Button 优先使用 '@/components/ui/button',不要在页面中直接使用 '@tarojs/components' 的 Button。",
|
||
},
|
||
// Input组件限制已禁用,因为UI组件库不支持date类型
|
||
// {
|
||
// selector:
|
||
// "ImportDeclaration[source.value='@tarojs/components'] ImportSpecifier[imported.name='Input']",
|
||
// message:
|
||
// "组件规范:Input 优先使用 '@/components/ui/input',不要在页面中直接使用 '@tarojs/components' 的 Input。",
|
||
// },
|
||
{
|
||
selector:
|
||
"ImportDeclaration[source.value='@tarojs/components'] ImportSpecifier[imported.name='Textarea']",
|
||
message:
|
||
"组件规范:Textarea 优先使用 '@/components/ui/textarea',不要在页面中直接使用 '@tarojs/components' 的 Textarea。",
|
||
},
|
||
{
|
||
selector:
|
||
"ImportDeclaration[source.value='@tarojs/components'] ImportSpecifier[imported.name='Label']",
|
||
message:
|
||
"组件规范:Label 优先使用 '@/components/ui/label',不要在页面中直接使用 '@tarojs/components' 的 Label。",
|
||
},
|
||
{
|
||
selector:
|
||
"ImportDeclaration[source.value='@tarojs/components'] ImportSpecifier[imported.name='Switch']",
|
||
message:
|
||
"组件规范:Switch 优先使用 '@/components/ui/switch',不要在页面中直接使用 '@tarojs/components' 的 Switch。",
|
||
},
|
||
{
|
||
selector:
|
||
"ImportDeclaration[source.value='@tarojs/components'] ImportSpecifier[imported.name='Slider']",
|
||
message:
|
||
"组件规范:Slider 优先使用 '@/components/ui/slider',不要在页面中直接使用 '@tarojs/components' 的 Slider。",
|
||
},
|
||
{
|
||
selector:
|
||
"ImportDeclaration[source.value='@tarojs/components'] ImportSpecifier[imported.name='Progress']",
|
||
message:
|
||
"组件规范:Progress 优先使用 '@/components/ui/progress',不要在页面中直接使用 '@tarojs/components' 的 Progress。",
|
||
},
|
||
];
|
||
|
||
const indexPageRestrictedSyntaxRules = [
|
||
{
|
||
selector: 'JSXText[value=/\\s*应用开发中\\s*/]',
|
||
message:
|
||
'工程规范:检测到首页 (src/pages/index/index.tsx) 仍为默认占位页面,这会导致用户无法进入新增页面,请根据用户需求开发实际的首页功能与界面。如果已经开发了新的首页,也需要删除旧首页,并更新 src/app.config.ts 文件',
|
||
},
|
||
];
|
||
|
||
export default [
|
||
...compat.extends('taro/react'),
|
||
{
|
||
rules: {
|
||
'react/jsx-uses-react': 'off',
|
||
'react/react-in-jsx-scope': 'off',
|
||
'jsx-quotes': ['error', 'prefer-double'],
|
||
'react-hooks/exhaustive-deps': 'off',
|
||
'tailwindcss/classnames-order': 'off',
|
||
'tailwindcss/no-custom-classname': 'off',
|
||
},
|
||
},
|
||
{
|
||
files: ['src/**/*.{js,jsx,ts,tsx}'],
|
||
ignores: ['src/network.ts'],
|
||
rules: {
|
||
'no-restricted-syntax': ['error', ...baseRestrictedSyntaxRules],
|
||
'no-restricted-properties': [
|
||
'error',
|
||
{
|
||
object: 'Taro',
|
||
property: 'request',
|
||
message:
|
||
"工程规范:请使用 Network.request 替代 Taro.request,导入方式: import { Network } from '@/network'",
|
||
},
|
||
{
|
||
object: 'Taro',
|
||
property: 'uploadFile',
|
||
message:
|
||
"工程规范:请使用 Network.uploadFile 替代 Taro.uploadFile,导入方式: import { Network } from '@/network'",
|
||
},
|
||
{
|
||
object: 'Taro',
|
||
property: 'downloadFile',
|
||
message:
|
||
"工程规范:请使用 Network.downloadFile 替代 Taro.downloadFile,导入方式: import { Network } from '@/network'",
|
||
},
|
||
],
|
||
},
|
||
},
|
||
{
|
||
files: ['src/**/*.css'],
|
||
plugins: {
|
||
local: cssImportGuardPlugin,
|
||
},
|
||
processor: 'local/css',
|
||
rules: {
|
||
'no-undef': 'off',
|
||
'no-restricted-syntax': ['error', ...baseRestrictedSyntaxRules],
|
||
},
|
||
},
|
||
{
|
||
files: ['src/pages/**/*.tsx'],
|
||
rules: {
|
||
'no-restricted-syntax': [
|
||
'error',
|
||
...baseRestrictedSyntaxRules,
|
||
...pageRestrictedSyntaxRules,
|
||
],
|
||
},
|
||
},
|
||
{
|
||
files: ['src/pages/index/index.tsx'],
|
||
rules: {
|
||
'no-restricted-syntax': [
|
||
'error',
|
||
...baseRestrictedSyntaxRules,
|
||
...pageRestrictedSyntaxRules,
|
||
...indexPageRestrictedSyntaxRules,
|
||
],
|
||
},
|
||
},
|
||
{
|
||
ignores: ['dist/**', 'dist-*/**', 'node_modules/**'],
|
||
},
|
||
];
|