fb348f3740
- 迁移前端源码 (src/) - 迁移后端服务 (server/) - 迁移配置文件 (package.json, tsconfig.json 等) - 更新需求概要文档 - 更新架构设计文档 - 更新接口定义文档 - 更新环境配置文档 - 创建测试目录结构和配置 项目技术栈: - Taro 4.1.9 (跨端框架) - React 18 - TypeScript - NestJS (后端) - Tailwind CSS 4 - shadcn/ui 组件库
252 lines
9.8 KiB
JavaScript
252 lines
9.8 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。",
|
|
},
|
|
{
|
|
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/**'],
|
|
},
|
|
];
|