22 changed files with 1360 additions and 0 deletions
@ -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); |
|||
} |
|||
})(); |
|||
@ -0,0 +1,37 @@ |
|||
FROM node:22-slim AS builder |
|||
|
|||
# --max-old-space-size |
|||
ENV PNPM_HOME="/pnpm" |
|||
ENV PATH="$PNPM_HOME:$PATH" |
|||
ENV NODE_OPTIONS=--max-old-space-size=8192 |
|||
ENV TZ=Asia/Shanghai |
|||
|
|||
RUN npm i -g corepack |
|||
|
|||
WORKDIR /app |
|||
|
|||
# copy package.json and pnpm-lock.yaml to workspace |
|||
COPY . /app |
|||
|
|||
# 安装依赖 |
|||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile |
|||
RUN pnpm run build --filter=\!./docs |
|||
|
|||
RUN echo "Builder Success 🎉" |
|||
|
|||
FROM nginx:stable-alpine AS production |
|||
|
|||
# 配置 nginx |
|||
RUN echo "types { application/javascript js mjs; }" > /etc/nginx/conf.d/mjs.conf \ |
|||
&& rm -rf /etc/nginx/conf.d/default.conf |
|||
|
|||
# 复制构建产物 |
|||
COPY --from=builder /app/playground/dist /usr/share/nginx/html |
|||
|
|||
# 复制 nginx 配置 |
|||
COPY --from=builder /app/scripts/deploy/nginx.conf /etc/nginx/nginx.conf |
|||
|
|||
EXPOSE 8080 |
|||
|
|||
# 启动 nginx |
|||
CMD ["nginx", "-g", "daemon off;"] |
|||
@ -0,0 +1,55 @@ |
|||
#!/bin/bash |
|||
|
|||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) |
|||
LOG_FILE=${SCRIPT_DIR}/build-local-docker-image.log |
|||
ERROR="" |
|||
IMAGE_NAME="vben-admin-local" |
|||
|
|||
function stop_and_remove_container() { |
|||
# Stop and remove the existing container |
|||
docker stop ${IMAGE_NAME} >/dev/null 2>&1 |
|||
docker rm ${IMAGE_NAME} >/dev/null 2>&1 |
|||
} |
|||
|
|||
function remove_image() { |
|||
# Remove the existing image |
|||
docker rmi vben-admin-pro >/dev/null 2>&1 |
|||
} |
|||
|
|||
function install_dependencies() { |
|||
# Install all dependencies |
|||
cd ${SCRIPT_DIR} |
|||
pnpm install || ERROR="install_dependencies failed" |
|||
} |
|||
|
|||
function build_image() { |
|||
# build docker |
|||
docker build ../../ -f Dockerfile -t ${IMAGE_NAME} || ERROR="build_image failed" |
|||
} |
|||
|
|||
function log_message() { |
|||
if [[ ${ERROR} != "" ]]; |
|||
then |
|||
>&2 echo "build failed, Please check build-local-docker-image.log for more details" |
|||
>&2 echo "ERROR: ${ERROR}" |
|||
exit 1 |
|||
else |
|||
echo "docker image with tag '${IMAGE_NAME}' built sussessfully. Use below sample command to run the container" |
|||
echo "" |
|||
echo "docker run -d -p 8010:8080 --name ${IMAGE_NAME} ${IMAGE_NAME}" |
|||
fi |
|||
} |
|||
|
|||
echo "Info: Stopping and removing existing container and image" | tee ${LOG_FILE} |
|||
stop_and_remove_container |
|||
remove_image |
|||
|
|||
echo "Info: Installing dependencies" | tee -a ${LOG_FILE} |
|||
install_dependencies 1>> ${LOG_FILE} 2>> ${LOG_FILE} |
|||
|
|||
if [[ ${ERROR} == "" ]]; then |
|||
echo "Info: Building docker image" | tee -a ${LOG_FILE} |
|||
build_image 1>> ${LOG_FILE} 2>> ${LOG_FILE} |
|||
fi |
|||
|
|||
log_message | tee -a ${LOG_FILE} |
|||
@ -0,0 +1,75 @@ |
|||
|
|||
#user nobody; |
|||
worker_processes 1; |
|||
|
|||
#error_log logs/error.log; |
|||
#error_log logs/error.log notice; |
|||
#error_log logs/error.log info; |
|||
|
|||
#pid logs/nginx.pid; |
|||
|
|||
|
|||
events { |
|||
worker_connections 1024; |
|||
} |
|||
|
|||
|
|||
http { |
|||
include mime.types; |
|||
default_type application/octet-stream; |
|||
|
|||
types { |
|||
application/javascript js mjs; |
|||
text/css css; |
|||
text/html html; |
|||
} |
|||
|
|||
sendfile on; |
|||
# tcp_nopush on; |
|||
|
|||
#keepalive_timeout 0; |
|||
# keepalive_timeout 65; |
|||
|
|||
# gzip on; |
|||
# gzip_buffers 32 16k; |
|||
# gzip_comp_level 6; |
|||
# gzip_min_length 1k; |
|||
# gzip_static on; |
|||
# gzip_types text/plain |
|||
# text/css |
|||
# application/javascript |
|||
# application/json |
|||
# application/x-javascript |
|||
# text/xml |
|||
# application/xml |
|||
# application/xml+rss |
|||
# text/javascript; #设置压缩的文件类型 |
|||
# gzip_vary on; |
|||
|
|||
server { |
|||
listen 8080; |
|||
server_name localhost; |
|||
|
|||
location / { |
|||
root /usr/share/nginx/html; |
|||
try_files $uri $uri/ /index.html; |
|||
index index.html; |
|||
# Enable CORS |
|||
add_header 'Access-Control-Allow-Origin' '*'; |
|||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; |
|||
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; |
|||
if ($request_method = 'OPTIONS') { |
|||
add_header 'Access-Control-Max-Age' 1728000; |
|||
add_header 'Content-Type' 'text/plain charset=UTF-8'; |
|||
add_header 'Content-Length' 0; |
|||
return 204; |
|||
} |
|||
} |
|||
|
|||
error_page 500 502 503 504 /50x.html; |
|||
|
|||
location = /50x.html { |
|||
root /usr/share/nginx/html; |
|||
} |
|||
} |
|||
} |
|||
@ -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 项目的根目录下运行 |
|||
@ -0,0 +1,3 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
import('../dist/index.mjs'); |
|||
@ -0,0 +1,7 @@ |
|||
import { defineBuildConfig } from 'unbuild'; |
|||
|
|||
export default defineBuildConfig({ |
|||
clean: true, |
|||
declaration: true, |
|||
entries: ['src/index'], |
|||
}); |
|||
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "@vben/turbo-run", |
|||
"version": "5.6.0", |
|||
"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:" |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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;
|
|||
// }
|
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/node.json", |
|||
"include": ["src"], |
|||
"exclude": ["node_modules"] |
|||
} |
|||
@ -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`: 检查包发布配置 |
|||
@ -0,0 +1,3 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
import('../dist/index.mjs'); |
|||
@ -0,0 +1,7 @@ |
|||
import { defineBuildConfig } from 'unbuild'; |
|||
|
|||
export default defineBuildConfig({ |
|||
clean: true, |
|||
declaration: true, |
|||
entries: ['src/index'], |
|||
}); |
|||
@ -0,0 +1,31 @@ |
|||
{ |
|||
"name": "@vben/vsh", |
|||
"version": "5.6.0", |
|||
"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:" |
|||
} |
|||
} |
|||
@ -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 }; |
|||
@ -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 }; |
|||
@ -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 }; |
|||
@ -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); |
|||
}); |
|||
@ -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 }; |
|||
@ -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 }; |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/tsconfig", |
|||
"extends": "@vben/tsconfig/node.json", |
|||
"include": ["src"], |
|||
"exclude": ["node_modules"] |
|||
} |
|||
Loading…
Reference in new issue