feat: 初始化项目基础结构和配置

This commit is contained in:
2026-01-06 17:56:57 +08:00
commit 397706bf2d
949 changed files with 80481 additions and 0 deletions

141
scripts/clean.mjs Normal file
View File

@@ -0,0 +1,141 @@
import { promises as fs } from 'node:fs';
import { join, normalize } from 'node:path';
const rootDir = process.cwd();
// 控制并发数量,避免创建过多的并发任务
const CONCURRENCY_LIMIT = 10;
// 需要跳过的目录,避免进入这些目录进行清理
const SKIP_DIRS = new Set(['.DS_Store', '.git', '.idea', '.vscode']);
/**
* 处理单个文件/目录项
* @param {string} currentDir - 当前目录路径
* @param {string} item - 文件/目录名
* @param {string[]} targets - 要删除的目标列表
* @param {number} _depth - 当前递归深度
* @returns {Promise<boolean>} - 是否需要进一步递归处理
*/
async function processItem(currentDir, item, targets, _depth) {
// 跳过特殊目录
if (SKIP_DIRS.has(item)) {
return false;
}
try {
const itemPath = normalize(join(currentDir, item));
if (targets.includes(item)) {
// 匹配到目标目录或文件时直接删除
await fs.rm(itemPath, { force: true, recursive: true });
console.log(`✅ Deleted: ${itemPath}`);
return false; // 已删除,无需递归
}
// 使用 readdir 的 withFileTypes 选项,避免额外的 lstat 调用
return true; // 可能需要递归,由调用方决定
} catch (error) {
// 更详细的错误信息
if (error.code === 'ENOENT') {
// 文件不存在,可能已被删除,这是正常情况
return false;
} else if (error.code === 'EPERM' || error.code === 'EACCES') {
console.error(`❌ Permission denied: ${item} in ${currentDir}`);
} else {
console.error(
`❌ Error handling item ${item} in ${currentDir}: ${error.message}`,
);
}
return false;
}
}
/**
* 递归查找并删除目标目录(并发优化版本)
* @param {string} currentDir - 当前遍历的目录路径
* @param {string[]} targets - 要删除的目标列表
* @param {number} depth - 当前递归深度,避免过深递归
*/
async function cleanTargetsRecursively(currentDir, targets, depth = 0) {
// 限制递归深度,避免无限递归
if (depth > 10) {
console.warn(`Max recursion depth reached at: ${currentDir}`);
return;
}
let dirents;
try {
// 使用 withFileTypes 选项,一次性获取文件类型信息,避免后续 lstat 调用
dirents = await fs.readdir(currentDir, { withFileTypes: true });
} catch (error) {
// 如果无法读取目录,可能已被删除或权限不足
console.warn(`Cannot read directory ${currentDir}: ${error.message}`);
return;
}
// 分批处理,控制并发数量
for (let i = 0; i < dirents.length; i += CONCURRENCY_LIMIT) {
const batch = dirents.slice(i, i + CONCURRENCY_LIMIT);
const tasks = batch.map(async (dirent) => {
const item = dirent.name;
const shouldRecurse = await processItem(currentDir, item, targets, depth);
// 如果是目录且没有被删除,则递归处理
if (shouldRecurse && dirent.isDirectory()) {
const itemPath = normalize(join(currentDir, item));
return cleanTargetsRecursively(itemPath, targets, depth + 1);
}
return null;
});
// 并发执行当前批次的任务
const results = await Promise.allSettled(tasks);
// 检查是否有失败的任务(可选:用于调试)
const failedTasks = results.filter(
(result) => result.status === 'rejected',
);
if (failedTasks.length > 0) {
console.warn(
`${failedTasks.length} tasks failed in batch starting at index ${i} in directory: ${currentDir}`,
);
}
}
}
(async function startCleanup() {
// 要删除的目录及文件名称
const targets = ['node_modules', 'dist', '.turbo', 'dist.zip'];
const deleteLockFile = process.argv.includes('--del-lock');
const cleanupTargets = [...targets];
if (deleteLockFile) {
cleanupTargets.push('pnpm-lock.yaml');
}
console.log(
`🚀 Starting cleanup of targets: ${cleanupTargets.join(', ')} from root: ${rootDir}`,
);
const startTime = Date.now();
try {
// 先统计要删除的目标数量
console.log('📊 Scanning for cleanup targets...');
await cleanTargetsRecursively(rootDir, cleanupTargets);
const endTime = Date.now();
const duration = (endTime - startTime) / 1000;
console.log(
`✨ Cleanup process completed successfully in ${duration.toFixed(2)}s`,
);
} catch (error) {
console.error(`💥 Unexpected error during cleanup: ${error.message}`);
process.exit(1);
}
})();

View File

@@ -0,0 +1,59 @@
# @vben/turbo-run
`turbo-run` 是一个命令行工具,允许你在多个包中并行运行命令。它提供了一个交互式的界面,让你可以选择要运行命令的包。
## 特性
- 🚀 交互式选择要运行的包
- 📦 支持 monorepo 项目结构
- 🔍 自动检测可用的命令
- 🎯 精确过滤目标包
## 安装
```bash
pnpm add -D @vben/turbo-run
```
## 使用方法
基本语法:
```bash
turbo-run [script]
```
例如,如果你想运行 `dev` 命令:
```bash
turbo-run dev
```
工具会自动检测哪些包有 `dev` 命令,并提供一个交互式界面让你选择要运行的包。
## 示例
假设你的项目中有以下包:
- `@vben/app`
- `@vben/admin`
- `@vben/website`
当你运行:
```bash
turbo-run dev
```
工具会:
1. 检测哪些包有 `dev` 命令
2. 显示一个交互式选择界面
3. 让你选择要运行命令的包
4. 使用 `pnpm --filter` 在选定的包中运行命令
## 注意事项
- 确保你的项目使用 pnpm 作为包管理器
- 确保目标包在 `package.json` 中定义了相应的脚本命令
- 该工具需要在 monorepo 项目的根目录下运行

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
import('../dist/index.mjs');

View File

@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View File

@@ -0,0 +1,29 @@
{
"name": "@vben/turbo-run",
"version": "5.5.9",
"private": true,
"license": "MIT",
"type": "module",
"scripts": {
"stub": "pnpm unbuild --stub"
},
"files": [
"dist"
],
"bin": {
"turbo-run": "./bin/turbo-run.mjs"
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"default": "./dist/index.mjs"
},
"./package.json": "./package.json"
},
"dependencies": {
"@clack/prompts": "catalog:",
"@vben/node-utils": "workspace:*",
"cac": "catalog:"
}
}

View File

@@ -0,0 +1,29 @@
import { colors, consola } from '@vben/node-utils';
import { cac } from 'cac';
import { run } from './run';
try {
const turboRun = cac('turbo-run');
turboRun
.command('[script]')
.usage(`Run turbo interactively.`)
.action(async (command: string) => {
run({ command });
});
// Invalid command
turboRun.on('command:*', () => {
consola.error(colors.red('Invalid command!'));
process.exit(1);
});
turboRun.usage('turbo-run');
turboRun.help();
turboRun.parse();
} catch (error) {
consola.error(error);
process.exit(1);
}

View File

@@ -0,0 +1,67 @@
import { execaCommand, getPackages } from '@vben/node-utils';
import { cancel, isCancel, select } from '@clack/prompts';
interface RunOptions {
command?: string;
}
export async function run(options: RunOptions) {
const { command } = options;
if (!command) {
console.error('Please enter the command to run');
process.exit(1);
}
const { packages } = await getPackages();
// const appPkgs = await findApps(process.cwd(), packages);
// const websitePkg = packages.find(
// (item) => item.packageJson.name === '@vben/website',
// );
// 只显示有对应命令的包
const selectPkgs = packages.filter((pkg) => {
return (pkg?.packageJson as Record<string, any>)?.scripts?.[command];
});
let selectPkg: string | symbol;
if (selectPkgs.length > 1) {
selectPkg = await select<string>({
message: `Select the app you need to run [${command}]:`,
options: selectPkgs.map((item) => ({
label: item?.packageJson.name,
value: item?.packageJson.name,
})),
});
if (isCancel(selectPkg) || !selectPkg) {
cancel('👋 Has cancelled');
process.exit(0);
}
} else {
selectPkg = selectPkgs[0]?.packageJson?.name ?? '';
}
if (!selectPkg) {
console.error('No app found');
process.exit(1);
}
execaCommand(`pnpm --filter=${selectPkg} run ${command}`, {
stdio: 'inherit',
});
}
/**
* 过滤app包
* @param root
* @param packages
*/
// async function findApps(root: string, packages: Package[]) {
// // apps内的
// const appPackages = packages.filter((pkg) => {
// const viteConfigExists = fs.existsSync(join(pkg.dir, 'vite.config.mts'));
// return pkg.dir.startsWith(join(root, 'apps')) && viteConfigExists;
// });
// return appPackages;
// }

View File

@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"include": ["src"],
"exclude": ["node_modules"]
}

56
scripts/vsh/README.md Normal file
View File

@@ -0,0 +1,56 @@
# @vben/vsh
一个 Shell 脚本工具集合,用于 Vue Vben Admin 项目的开发和管理。
## 功能特性
- 🚀 基于 Node.js 的现代化 Shell 工具
- 📦 支持模块化开发和按需加载
- 🔍 提供依赖检查和分析功能
- 🔄 支持循环依赖扫描
- 📝 提供包发布检查功能
## 安装
```bash
# 使用 pnpm 安装
pnpm add -D @vben/vsh
# 或者使用 npm
npm install -D @vben/vsh
# 或者使用 yarn
yarn add -D @vben/vsh
```
## 使用方法
### 全局安装
```bash
# 全局安装
pnpm add -g @vben/vsh
# 使用 vsh 命令
vsh [command]
```
### 本地使用
```bash
# 在 package.json 中添加脚本
{
"scripts": {
"vsh": "vsh"
}
}
# 运行命令
pnpm vsh [command]
```
## 命令列表
- `vsh check-deps`: 检查项目依赖
- `vsh scan-circular`: 扫描循环依赖
- `vsh publish-check`: 检查包发布配置

3
scripts/vsh/bin/vsh.mjs Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
import('../dist/index.mjs');

View File

@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

31
scripts/vsh/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "@vben/vsh",
"version": "5.5.9",
"private": true,
"license": "MIT",
"type": "module",
"scripts": {
"stub": "pnpm unbuild --stub"
},
"files": [
"dist"
],
"bin": {
"vsh": "./bin/vsh.mjs"
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"default": "./dist/index.mjs"
},
"./package.json": "./package.json"
},
"dependencies": {
"@vben/node-utils": "workspace:*",
"cac": "catalog:",
"circular-dependency-scanner": "catalog:",
"depcheck": "catalog:",
"publint": "catalog:"
}
}

View File

@@ -0,0 +1,170 @@
import type { CAC } from 'cac';
import { extname } from 'node:path';
import { getStagedFiles } from '@vben/node-utils';
import { circularDepsDetect } from 'circular-dependency-scanner';
// 默认配置
const DEFAULT_CONFIG = {
allowedExtensions: ['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],
ignoreDirs: [
'dist',
'.turbo',
'output',
'.cache',
'scripts',
'internal',
'packages/effects/request/src/',
'packages/@core/ui-kit/menu-ui/src/',
'packages/@core/ui-kit/popup-ui/src/',
],
threshold: 0, // 循环依赖的阈值
} as const;
// 类型定义
type CircularDependencyResult = string[];
interface CheckCircularConfig {
allowedExtensions?: string[];
ignoreDirs?: string[];
threshold?: number;
}
interface CommandOptions {
config?: CheckCircularConfig;
staged: boolean;
verbose: boolean;
}
// 缓存机制
const cache = new Map<string, CircularDependencyResult[]>();
/**
* 格式化循环依赖的输出
* @param circles - 循环依赖结果
*/
function formatCircles(circles: CircularDependencyResult[]): void {
if (circles.length === 0) {
console.log('✅ No circular dependencies found');
return;
}
console.log('⚠️ Circular dependencies found:');
circles.forEach((circle, index) => {
console.log(`\nCircular dependency #${index + 1}:`);
circle.forEach((file) => console.log(`${file}`));
});
}
/**
* 检查项目中的循环依赖
* @param options - 检查选项
* @param options.staged - 是否只检查暂存区文件
* @param options.verbose - 是否显示详细信息
* @param options.config - 自定义配置
* @returns Promise<void>
*/
async function checkCircular({
config = {},
staged,
verbose,
}: CommandOptions): Promise<void> {
try {
// 合并配置
const finalConfig = {
...DEFAULT_CONFIG,
...config,
};
// 生成忽略模式
const ignorePattern = `**/{${finalConfig.ignoreDirs.join(',')}}/**`;
// 检查缓存
const cacheKey = `${staged}-${process.cwd()}-${ignorePattern}`;
if (cache.has(cacheKey)) {
const cachedResults = cache.get(cacheKey);
if (cachedResults) {
verbose && formatCircles(cachedResults);
}
return;
}
// 检测循环依赖
const results = await circularDepsDetect({
absolute: staged,
cwd: process.cwd(),
ignore: [ignorePattern],
});
if (staged) {
let files = await getStagedFiles();
const allowedExtensions = new Set(finalConfig.allowedExtensions);
// 过滤文件列表
files = files.filter((file) => allowedExtensions.has(extname(file)));
const circularFiles: CircularDependencyResult[] = [];
for (const file of files) {
for (const result of results) {
const resultFiles = result.flat();
if (resultFiles.includes(file)) {
circularFiles.push(result);
}
}
}
// 更新缓存
cache.set(cacheKey, circularFiles);
verbose && formatCircles(circularFiles);
} else {
// 更新缓存
cache.set(cacheKey, results);
verbose && formatCircles(results);
}
// 如果发现循环依赖,只输出警告信息
if (results.length > 0) {
console.log(
'\n⚠ Warning: Circular dependencies found, please check and fix',
);
}
} catch (error) {
console.error(
'❌ Error checking circular dependencies:',
error instanceof Error ? error.message : error,
);
}
}
/**
* 定义检查循环依赖的命令
* @param cac - CAC实例
*/
function defineCheckCircularCommand(cac: CAC): void {
cac
.command('check-circular')
.option('--staged', 'Only check staged files')
.option('--verbose', 'Show detailed information')
.option('--threshold <number>', 'Threshold for circular dependencies', {
default: 0,
})
.option('--ignore-dirs <dirs>', 'Directories to ignore, comma separated')
.usage('Analyze project circular dependencies')
.action(async ({ ignoreDirs, staged, threshold, verbose }) => {
const config: CheckCircularConfig = {
threshold: Number(threshold),
...(ignoreDirs && { ignoreDirs: ignoreDirs.split(',') }),
};
await checkCircular({
config,
staged,
verbose: verbose ?? true,
});
});
}
export { type CheckCircularConfig, defineCheckCircularCommand };

View File

@@ -0,0 +1,194 @@
import type { CAC } from 'cac';
import { getPackages } from '@vben/node-utils';
import depcheck from 'depcheck';
// 默认配置
const DEFAULT_CONFIG = {
// 需要忽略的依赖匹配
ignoreMatches: [
'vite',
'vitest',
'unbuild',
'@vben/tsconfig',
'@vben/vite-config',
'@vben/tailwind-config',
'@types/*',
'@vben-core/design',
],
// 需要忽略的包
ignorePackages: [
'@vben/backend-mock',
'@vben/commitlint-config',
'@vben/eslint-config',
'@vben/node-utils',
'@vben/prettier-config',
'@vben/stylelint-config',
'@vben/tailwind-config',
'@vben/tsconfig',
'@vben/vite-config',
'@vben/vsh',
],
// 需要忽略的文件模式
ignorePatterns: ['dist', 'node_modules', 'public'],
};
interface DepcheckResult {
dependencies: string[];
devDependencies: string[];
missing: Record<string, string[]>;
}
interface DepcheckConfig {
ignoreMatches?: string[];
ignorePackages?: string[];
ignorePatterns?: string[];
}
interface PackageInfo {
dir: string;
packageJson: {
name: string;
};
}
/**
* 清理依赖检查结果
* @param unused - 依赖检查结果
*/
function cleanDepcheckResult(unused: DepcheckResult): void {
// 删除file:前缀的依赖提示,该依赖是本地依赖
Reflect.deleteProperty(unused.missing, 'file:');
// 清理路径依赖
Object.keys(unused.missing).forEach((key) => {
unused.missing[key] = (unused.missing[key] || []).filter(
(item: string) => !item.startsWith('/'),
);
if (unused.missing[key].length === 0) {
Reflect.deleteProperty(unused.missing, key);
}
});
}
/**
* 格式化依赖检查结果
* @param pkgName - 包名
* @param unused - 依赖检查结果
*/
function formatDepcheckResult(pkgName: string, unused: DepcheckResult): void {
const hasIssues =
Object.keys(unused.missing).length > 0 ||
unused.dependencies.length > 0 ||
unused.devDependencies.length > 0;
if (!hasIssues) {
return;
}
console.log('\n📦 Package:', pkgName);
if (Object.keys(unused.missing).length > 0) {
console.log('❌ Missing dependencies:');
Object.entries(unused.missing).forEach(([dep, files]) => {
console.log(` - ${dep}:`);
files.forEach((file) => console.log(`${file}`));
});
}
if (unused.dependencies.length > 0) {
console.log('⚠️ Unused dependencies:');
unused.dependencies.forEach((dep) => console.log(` - ${dep}`));
}
if (unused.devDependencies.length > 0) {
console.log('⚠️ Unused devDependencies:');
unused.devDependencies.forEach((dep) => console.log(` - ${dep}`));
}
}
/**
* 运行依赖检查
* @param config - 配置选项
*/
async function runDepcheck(config: DepcheckConfig = {}): Promise<void> {
try {
const finalConfig = {
...DEFAULT_CONFIG,
...config,
};
const { packages } = await getPackages();
let hasIssues = false;
await Promise.all(
packages.map(async (pkg: PackageInfo) => {
// 跳过需要忽略的包
if (finalConfig.ignorePackages.includes(pkg.packageJson.name)) {
return;
}
const unused = await depcheck(pkg.dir, {
ignoreMatches: finalConfig.ignoreMatches,
ignorePatterns: finalConfig.ignorePatterns,
});
cleanDepcheckResult(unused);
const pkgHasIssues =
Object.keys(unused.missing).length > 0 ||
unused.dependencies.length > 0 ||
unused.devDependencies.length > 0;
if (pkgHasIssues) {
hasIssues = true;
formatDepcheckResult(pkg.packageJson.name, unused);
}
}),
);
if (!hasIssues) {
console.log('\n✅ Dependency check completed, no issues found');
}
} catch (error) {
console.error(
'❌ Dependency check failed:',
error instanceof Error ? error.message : error,
);
}
}
/**
* 定义依赖检查命令
* @param cac - CAC实例
*/
function defineDepcheckCommand(cac: CAC): void {
cac
.command('check-dep')
.option(
'--ignore-packages <packages>',
'Packages to ignore, comma separated',
)
.option(
'--ignore-matches <matches>',
'Dependency patterns to ignore, comma separated',
)
.option(
'--ignore-patterns <patterns>',
'File patterns to ignore, comma separated',
)
.usage('Analyze project dependencies')
.action(async ({ ignoreMatches, ignorePackages, ignorePatterns }) => {
const config: DepcheckConfig = {
...(ignorePackages && { ignorePackages: ignorePackages.split(',') }),
...(ignoreMatches && { ignoreMatches: ignoreMatches.split(',') }),
...(ignorePatterns && { ignorePatterns: ignorePatterns.split(',') }),
};
await runDepcheck(config);
});
}
export { defineDepcheckCommand, type DepcheckConfig };

View File

@@ -0,0 +1,78 @@
import type { CAC } from 'cac';
import { join, relative } from 'node:path';
import {
colors,
consola,
findMonorepoRoot,
getPackages,
gitAdd,
outputJSON,
prettierFormat,
toPosixPath,
} from '@vben/node-utils';
const CODE_WORKSPACE_FILE = join('vben-admin.code-workspace');
interface CodeWorkspaceCommandOptions {
autoCommit?: boolean;
spaces?: number;
}
async function createCodeWorkspace({
autoCommit = false,
spaces = 2,
}: CodeWorkspaceCommandOptions) {
const { packages, rootDir } = await getPackages();
let folders = packages.map((pkg) => {
const { dir, packageJson } = pkg;
return {
name: packageJson.name,
path: toPosixPath(relative(rootDir, dir)),
};
});
folders = folders.filter(Boolean);
const monorepoRoot = findMonorepoRoot();
const outputPath = join(monorepoRoot, CODE_WORKSPACE_FILE);
await outputJSON(outputPath, { folders }, spaces);
await prettierFormat(outputPath);
if (autoCommit) {
await gitAdd(CODE_WORKSPACE_FILE, monorepoRoot);
}
}
async function runCodeWorkspace({
autoCommit,
spaces,
}: CodeWorkspaceCommandOptions) {
await createCodeWorkspace({
autoCommit,
spaces,
});
if (autoCommit) {
return;
}
consola.log('');
consola.success(colors.green(`${CODE_WORKSPACE_FILE} is updated!`));
consola.log('');
}
function defineCodeWorkspaceCommand(cac: CAC) {
cac
.command('code-workspace')
.usage('Update the `.code-workspace` file')
.option('--spaces [number]', '.code-workspace JSON file spaces.', {
default: 2,
})
.option('--auto-commit', 'auto commit .code-workspace JSON file.', {
default: false,
})
.action(runCodeWorkspace);
}
export { defineCodeWorkspaceCommand };

74
scripts/vsh/src/index.ts Normal file
View File

@@ -0,0 +1,74 @@
import { colors, consola } from '@vben/node-utils';
import { cac } from 'cac';
import { version } from '../package.json';
import { defineCheckCircularCommand } from './check-circular';
import { defineDepcheckCommand } from './check-dep';
import { defineCodeWorkspaceCommand } from './code-workspace';
import { defineLintCommand } from './lint';
import { definePubLintCommand } from './publint';
// 命令描述
const COMMAND_DESCRIPTIONS = {
'check-circular': 'Check for circular dependencies',
'check-dep': 'Check for unused dependencies',
'code-workspace': 'Manage VS Code workspace settings',
lint: 'Run linting on the project',
publint: 'Check package.json files for publishing standards',
} as const;
/**
* Initialize and run the CLI
*/
async function main(): Promise<void> {
try {
const vsh = cac('vsh');
// Register commands
defineLintCommand(vsh);
definePubLintCommand(vsh);
defineCodeWorkspaceCommand(vsh);
defineCheckCircularCommand(vsh);
defineDepcheckCommand(vsh);
// Handle invalid commands
vsh.on('command:*', ([cmd]) => {
consola.error(
colors.red(`Invalid command: ${cmd}`),
'\n',
colors.yellow('Available commands:'),
'\n',
Object.entries(COMMAND_DESCRIPTIONS)
.map(([cmd, desc]) => ` ${colors.cyan(cmd)} - ${desc}`)
.join('\n'),
);
process.exit(1);
});
// Set up CLI
vsh.usage('vsh <command> [options]');
vsh.help();
vsh.version(version);
// Parse arguments
vsh.parse();
} catch (error) {
consola.error(
colors.red('An unexpected error occurred:'),
'\n',
error instanceof Error ? error.message : error,
);
process.exit(1);
}
}
// Run the CLI
main().catch((error) => {
consola.error(
colors.red('Failed to start CLI:'),
'\n',
error instanceof Error ? error.message : error,
);
process.exit(1);
});

View File

@@ -0,0 +1,48 @@
import type { CAC } from 'cac';
import { execaCommand } from '@vben/node-utils';
interface LintCommandOptions {
/**
* Format lint problem.
*/
format?: boolean;
}
async function runLint({ format }: LintCommandOptions) {
// process.env.FORCE_COLOR = '3';
if (format) {
await execaCommand(`stylelint "**/*.{vue,css,less,scss}" --cache --fix`, {
stdio: 'inherit',
});
await execaCommand(`eslint . --cache --fix`, {
stdio: 'inherit',
});
await execaCommand(`prettier . --write --cache --log-level warn`, {
stdio: 'inherit',
});
return;
}
await Promise.all([
execaCommand(`eslint . --cache`, {
stdio: 'inherit',
}),
execaCommand(`prettier . --ignore-unknown --check --cache`, {
stdio: 'inherit',
}),
execaCommand(`stylelint "**/*.{vue,css,less,scss}" --cache`, {
stdio: 'inherit',
}),
]);
}
function defineLintCommand(cac: CAC) {
cac
.command('lint')
.usage('Batch execute project lint check.')
.option('--format', 'Format lint problem.')
.action(runLint);
}
export { defineLintCommand };

View File

@@ -0,0 +1,185 @@
import type { CAC } from 'cac';
import type { Result } from 'publint';
import { basename, dirname, join } from 'node:path';
import {
colors,
consola,
ensureFile,
findMonorepoRoot,
generatorContentHash,
getPackages,
outputJSON,
readJSON,
UNICODE,
} from '@vben/node-utils';
import { publint } from 'publint';
import { formatMessage } from 'publint/utils';
const CACHE_FILE = join(
'node_modules',
'.cache',
'publint',
'.pkglintcache.json',
);
interface PubLintCommandOptions {
/**
* Only errors are checked, no program exit is performed
*/
check?: boolean;
}
/**
* Get files that require lint
* @param files
*/
async function getLintFiles(files: string[] = []) {
const lintFiles: string[] = [];
if (files?.length > 0) {
return files.filter((file) => basename(file) === 'package.json');
}
const { packages } = await getPackages();
for (const { dir } of packages) {
lintFiles.push(join(dir, 'package.json'));
}
return lintFiles;
}
function getCacheFile() {
const root = findMonorepoRoot();
return join(root, CACHE_FILE);
}
async function readCache(cacheFile: string) {
try {
await ensureFile(cacheFile);
return await readJSON(cacheFile);
} catch {
return {};
}
}
async function runPublint(files: string[], { check }: PubLintCommandOptions) {
const lintFiles = await getLintFiles(files);
const cacheFile = getCacheFile();
const cacheData = await readCache(cacheFile);
const cache: Record<string, { hash: string; result: Result }> = cacheData;
const results = await Promise.all(
lintFiles.map(async (file) => {
try {
const pkgJson = await readJSON(file);
if (pkgJson.private) {
return null;
}
Reflect.deleteProperty(pkgJson, 'dependencies');
Reflect.deleteProperty(pkgJson, 'devDependencies');
Reflect.deleteProperty(pkgJson, 'peerDependencies');
const content = JSON.stringify(pkgJson);
const hash = generatorContentHash(content);
const publintResult: Result =
cache?.[file]?.hash === hash
? (cache?.[file]?.result ?? [])
: await publint({
level: 'suggestion',
pkgDir: dirname(file),
strict: true,
});
cache[file] = {
hash,
result: publintResult,
};
return { pkgJson, pkgPath: file, publintResult };
} catch {
return null;
}
}),
);
await outputJSON(cacheFile, cache);
printResult(results, check);
}
function printResult(
results: Array<null | {
pkgJson: Record<string, number | string>;
pkgPath: string;
publintResult: Result;
}>,
check?: boolean,
) {
let errorCount = 0;
let warningCount = 0;
let suggestionsCount = 0;
for (const result of results) {
if (!result) {
continue;
}
const { pkgJson, pkgPath, publintResult } = result;
const messages = publintResult?.messages ?? [];
if (messages?.length < 1) {
continue;
}
consola.log('');
consola.log(pkgPath);
for (const message of messages) {
switch (message.type) {
case 'error': {
errorCount++;
break;
}
case 'suggestion': {
suggestionsCount++;
break;
}
case 'warning': {
warningCount++;
break;
}
// No default
}
const ruleUrl = `https://publint.dev/rules#${message.code.toLocaleLowerCase()}`;
consola.log(
` ${formatMessage(message, pkgJson)}${colors.dim(` ${ruleUrl}`)}`,
);
}
}
const totalCount = warningCount + errorCount + suggestionsCount;
if (totalCount > 0) {
consola.error(
colors.red(
`${UNICODE.FAILURE} ${totalCount} problem (${errorCount} errors, ${warningCount} warnings, ${suggestionsCount} suggestions)`,
),
);
!check && process.exit(1);
} else {
consola.log(colors.green(`${UNICODE.SUCCESS} No problem`));
}
}
function definePubLintCommand(cac: CAC) {
cac
.command('publint [...files]')
.usage('Check if the monorepo package conforms to the publint standard.')
.option('--check', 'Only errors are checked, no program exit is performed.')
.action(runPublint);
}
export { definePubLintCommand };

View File

@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"include": ["src"],
"exclude": ["node_modules"]
}