提交qiming-dev-inject
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@
|
|||||||
!/qiming-file-server/
|
!/qiming-file-server/
|
||||||
!/qiming-mobile/
|
!/qiming-mobile/
|
||||||
!/qiming-claude-code-acp-ts/
|
!/qiming-claude-code-acp-ts/
|
||||||
|
!/qiming-dev-inject/
|
||||||
!/qimingclaw/
|
!/qimingclaw/
|
||||||
!/qimingcode/
|
!/qimingcode/
|
||||||
|
|
||||||
|
|||||||
76
qiming-dev-inject/.gitignore
vendored
Normal file
76
qiming-dev-inject/.gitignore
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# 依赖
|
||||||
|
node_modules/
|
||||||
|
.npm/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# 缓存
|
||||||
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
|
.npm/
|
||||||
|
.eslintcache
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# 操作系统
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# 编辑器
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.cursor/
|
||||||
|
|
||||||
|
# 环境变量
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env*.local
|
||||||
|
.env.development
|
||||||
|
.env.production
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# 测试项目依赖和构建
|
||||||
|
test-projects/*/node_modules/
|
||||||
|
test-projects/*/.next/
|
||||||
|
test-projects/*/dist/
|
||||||
|
test-projects/*/build/
|
||||||
|
test-projects/*/.env*
|
||||||
|
|
||||||
|
# 临时文件
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
server.log
|
||||||
|
|
||||||
|
# 打包文件
|
||||||
|
*.tgz
|
||||||
|
*.tar.gz
|
||||||
|
|
||||||
|
# 覆盖率报告
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# IDE 临时文件
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Lock files
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
73
qiming-dev-inject/.npmignore
Normal file
73
qiming-dev-inject/.npmignore
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# 测试文件和目录
|
||||||
|
test/
|
||||||
|
test-*.js
|
||||||
|
test.html
|
||||||
|
test-simplified.js
|
||||||
|
test-projects/
|
||||||
|
*.test.js
|
||||||
|
|
||||||
|
# 示例和演示文件
|
||||||
|
# EXAMPLES.md (保留在发布包中,因为用户可能需要)
|
||||||
|
react-iframe-test.html
|
||||||
|
simple-react-app.html
|
||||||
|
simplified-test.html
|
||||||
|
server.js
|
||||||
|
start-test.js
|
||||||
|
test-cli.js
|
||||||
|
test-framework.js
|
||||||
|
test-framework-simple.js
|
||||||
|
test-full-system.js
|
||||||
|
|
||||||
|
# 日志文件
|
||||||
|
*.log
|
||||||
|
server.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# 依赖
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# 构建输出
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# 缓存
|
||||||
|
.cache/
|
||||||
|
.npm/
|
||||||
|
.parcel-cache/
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# 环境配置
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# IDE 配置
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# 系统文件
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.github/
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.travis.yml
|
||||||
|
|
||||||
|
# 文档(除了 package.json 中 files 指定的)
|
||||||
|
# 注意:README.md 等会自动包含
|
||||||
149
qiming-dev-inject/EXAMPLES.md
Normal file
149
qiming-dev-inject/EXAMPLES.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# 使用示例
|
||||||
|
|
||||||
|
## 安装方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 静默安装(推荐,自动确认,无需交互)
|
||||||
|
npx -y @xagi/dev-inject install --framework
|
||||||
|
|
||||||
|
# 使用默认脚本地址 (/sdk/dev-monitor.js)
|
||||||
|
npx @xagi/dev-inject install --framework
|
||||||
|
|
||||||
|
# 使用自定义地址
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||||
|
|
||||||
|
# 使用 pnpm dlx
|
||||||
|
pnpm dlx @xagi/dev-inject install --framework
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. 本地开发环境监控
|
||||||
|
|
||||||
|
将监控脚本复制到你的项目静态资源目录:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 复制监控脚本到你的项目中
|
||||||
|
cp dev-inject/scripts/dev-monitor.js public/scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
然后注入到 HTML:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 注入本地监控脚本
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js
|
||||||
|
|
||||||
|
# 或者指定特定 HTML 文件
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js --file=./public/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 远程开发服务器监控
|
||||||
|
|
||||||
|
如果你的监控脚本运行在开发服务器上:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 注入远程脚本
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js
|
||||||
|
|
||||||
|
# 或使用 pnpm
|
||||||
|
pnpm dlx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 在 React/Vite/Next.js 项目中使用(框架感知模式)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vite 项目 - 使用框架感知注入(推荐)
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||||
|
|
||||||
|
# Next.js App Router 项目
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||||
|
|
||||||
|
# Next.js Pages Router 项目
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js --framework
|
||||||
|
|
||||||
|
# Create React App 项目
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js --framework
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 开发完成后清理
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 移除框架注入
|
||||||
|
npx @xagi/dev-inject uninstall --framework
|
||||||
|
|
||||||
|
# 移除传统注入
|
||||||
|
npx @xagi/dev-inject uninstall
|
||||||
|
|
||||||
|
# 只移除特定文件的注入脚本
|
||||||
|
pnpm dlx @xagi/dev-inject uninstall --file=./index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 预览模式
|
||||||
|
|
||||||
|
在实际注入前,可以先预览将要执行的操作:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js --dry-run --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
## 监控面板使用
|
||||||
|
|
||||||
|
在浏览器控制台中:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 显示监控面板
|
||||||
|
DevMonitor.showPanel()
|
||||||
|
|
||||||
|
// 获取监控数据
|
||||||
|
DevMonitor.getData()
|
||||||
|
|
||||||
|
// 导出数据到文件
|
||||||
|
DevMonitor.exportData()
|
||||||
|
|
||||||
|
// 清除监控数据
|
||||||
|
DevMonitor.clearData()
|
||||||
|
|
||||||
|
// 设置日志级别
|
||||||
|
DevMonitor.setLogLevel('debug') // debug, info, warn, error
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整工作流示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 进入项目目录
|
||||||
|
cd my-project
|
||||||
|
|
||||||
|
# 2. 预览注入操作(框架感知模式)
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework --dry-run
|
||||||
|
|
||||||
|
# 3. 执行注入(框架感知模式)
|
||||||
|
# 方式一:静默安装(推荐,一键完成)
|
||||||
|
npx -y @xagi/dev-inject install --framework
|
||||||
|
|
||||||
|
# 方式二:标准安装
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||||
|
|
||||||
|
# 4. 启动开发服务器
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 5. 在浏览器中测试功能,打开控制台使用 DevMonitor API
|
||||||
|
|
||||||
|
# 6. 开发完成后清理注入
|
||||||
|
npx @xagi/dev-inject uninstall --framework
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速启动示例
|
||||||
|
|
||||||
|
使用远程监控脚本(推荐):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 静默安装(最推荐,一键完成)
|
||||||
|
npx -y @xagi/dev-inject install --framework
|
||||||
|
|
||||||
|
# 框架感知注入(自动检测最佳注入方式)
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||||
|
|
||||||
|
# 或使用 pnpm
|
||||||
|
pnpm dlx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||||
|
|
||||||
|
# 卸载
|
||||||
|
npx @xagi/dev-inject uninstall --framework
|
||||||
|
```
|
||||||
22
qiming-dev-inject/LICENSE
Normal file
22
qiming-dev-inject/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 dongdada29
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
257
qiming-dev-inject/README.md
Normal file
257
qiming-dev-inject/README.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
# 网页应用智能脚本注入
|
||||||
|
|
||||||
|
🚀 **框架感知的开发环境脚本注入工具**
|
||||||
|
|
||||||
|
支持 Vite、Next.js、React 等现代框架的智能脚本注入,提供零侵入式的开发期监控方案。
|
||||||
|
|
||||||
|
## 🌟 特性
|
||||||
|
|
||||||
|
### 框架感知注入
|
||||||
|
- 🟢 **Vite**: 自动注入 Vite 插件到 `vite.config.js`
|
||||||
|
- 🟢 **Next.js App Router**: 注入到 `app/layout.tsx`
|
||||||
|
- 🟢 **Next.js Pages Router**: 注入到 `pages/_document.tsx`
|
||||||
|
- 🟢 **Create React App**: 注入到 `public/index.html`
|
||||||
|
- 🟢 **传统 HTML**: 直接修改 HTML 文件
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
- ✅ 支持远程 URL 和绝对路径注入
|
||||||
|
- ✅ 自动检测项目类型和最佳注入方式
|
||||||
|
- ✅ 智能去重,避免重复注入
|
||||||
|
- ✅ 支持卸载功能
|
||||||
|
- ✅ 预览模式(dry-run)
|
||||||
|
- ✅ 详细日志输出
|
||||||
|
- ✅ 零侵入式修改
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
使用 npx(推荐):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看帮助信息
|
||||||
|
npx @xagi/dev-inject --help
|
||||||
|
|
||||||
|
# 查看版本信息
|
||||||
|
npx @xagi/dev-inject --version
|
||||||
|
|
||||||
|
# 静默安装(自动确认,无需交互)-y 选项
|
||||||
|
npx -y @xagi/dev-inject install --framework
|
||||||
|
```
|
||||||
|
|
||||||
|
或使用 pnpm dlx:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dlx @xagi/dev-inject --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 框架感知注入(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 静默安装(推荐,无需确认)
|
||||||
|
npx -y @xagi/dev-inject install --framework
|
||||||
|
|
||||||
|
# 使用默认脚本地址 (/sdk/dev-monitor.js)
|
||||||
|
npx @xagi/dev-inject install --framework
|
||||||
|
|
||||||
|
# 或指定自定义地址
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||||
|
|
||||||
|
# 使用 pnpm
|
||||||
|
pnpm dlx @xagi/dev-inject install --framework
|
||||||
|
|
||||||
|
# 查看检测到的项目类型
|
||||||
|
npx @xagi/dev-inject install --framework --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### 传统 HTML 注入
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 注入远程脚本
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js
|
||||||
|
|
||||||
|
# 注入本地脚本
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js
|
||||||
|
|
||||||
|
# 指定特定文件
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/monitor.js --file=./public/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预览和卸载
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 预览将要执行的操作
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/monitor.js --framework --dry-run
|
||||||
|
|
||||||
|
# 移除框架注入
|
||||||
|
npx @xagi/dev-inject uninstall --framework
|
||||||
|
|
||||||
|
# 移除传统注入
|
||||||
|
npx @xagi/dev-inject uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 支持的脚本格式
|
||||||
|
|
||||||
|
### 远程 URL
|
||||||
|
```bash
|
||||||
|
--remote=http://localhost:9000/dev-monitor.js
|
||||||
|
--remote=https://cdn.example.com/monitor.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 绝对路径
|
||||||
|
```bash
|
||||||
|
--remote=/scripts/dev-monitor.js
|
||||||
|
--remote=/assets/monitor.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 配置选项
|
||||||
|
|
||||||
|
| 选项 | 描述 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `--remote` | 脚本地址(可选,默认值: /sdk/dev-monitor.js) | `--remote=/scripts/dev-monitor.js` |
|
||||||
|
| `--framework, -f` | 使用框架感知注入 | `--framework` |
|
||||||
|
| `--file` | 指定 HTML 文件 | `--file=./public/index.html` |
|
||||||
|
| `--dry-run` | 预览模式 | `--dry-run` |
|
||||||
|
| `--verbose` | 详细输出 | `--verbose` |
|
||||||
|
| `--help` | 显示帮助 | `--help` |
|
||||||
|
| `--version` | 显示版本 | `--version` |
|
||||||
|
|
||||||
|
## 监控脚本功能
|
||||||
|
|
||||||
|
`dev-inject/scripts/dev-monitor.js` 提供完整的开发监控功能:
|
||||||
|
|
||||||
|
### 错误监控
|
||||||
|
- 全局 JavaScript 错误捕获
|
||||||
|
- Promise Rejection 监控
|
||||||
|
- 资源加载错误监控
|
||||||
|
- iframe 通信错误
|
||||||
|
|
||||||
|
### 性能监控
|
||||||
|
- 页面加载时间分析
|
||||||
|
- 网络请求时间统计
|
||||||
|
- 长任务检测
|
||||||
|
- DOM Ready 性能
|
||||||
|
|
||||||
|
### 开发工具
|
||||||
|
- 实时监控面板
|
||||||
|
- 数据导出功能
|
||||||
|
- 控制台日志拦截
|
||||||
|
- iframe 通信支持
|
||||||
|
|
||||||
|
### 使用监控面板
|
||||||
|
```javascript
|
||||||
|
// 在浏览器控制台执行
|
||||||
|
DevMonitor.showPanel(); // 显示监控面板
|
||||||
|
DevMonitor.getData(); // 获取监控数据
|
||||||
|
DevMonitor.exportData(); // 导出监控数据
|
||||||
|
DevMonitor.clearData(); // 清除监控数据
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
| 选项 | 描述 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `--remote` | 脚本地址(必需) | `--remote=/scripts/dev-monitor.js` |
|
||||||
|
| `--file` | 指定 HTML 文件 | `--file=./public/index.html` |
|
||||||
|
| `--dry-run` | 预览模式 | `--dry-run` |
|
||||||
|
| `--verbose` | 详细输出 | `--verbose` |
|
||||||
|
| `--help` | 显示帮助 | `--help` |
|
||||||
|
| `--version` | 显示版本 | `--version` |
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
1. **查找 HTML 文件**:自动查找项目中的 HTML 文件(优先 index.html)
|
||||||
|
2. **脚本注入**:在 `</head>` 标签前注入脚本
|
||||||
|
3. **去重处理**:移除之前注入的相同脚本,避免重复
|
||||||
|
4. **智能检测**:支持多种项目结构(React、Vite、Next.js 等)
|
||||||
|
|
||||||
|
## 开发和测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入项目目录
|
||||||
|
cd dev-inject
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 链接到全局(用于本地测试)
|
||||||
|
npm link
|
||||||
|
|
||||||
|
# 测试帮助信息
|
||||||
|
dev-inject --help
|
||||||
|
|
||||||
|
# 测试注入功能(预览模式)
|
||||||
|
dev-inject install --remote=/scripts/dev-monitor.js --dry-run
|
||||||
|
|
||||||
|
# 取消链接
|
||||||
|
npm unlink
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 发布到 npm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 确保你已经登录 npm
|
||||||
|
npm login
|
||||||
|
|
||||||
|
# 发布到 npm
|
||||||
|
npm publish
|
||||||
|
|
||||||
|
# 发布后验证
|
||||||
|
npx @xagi/dev-inject --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
dev-inject/
|
||||||
|
├── bin/
|
||||||
|
│ └── index.js # CLI 入口文件
|
||||||
|
├── lib/
|
||||||
|
│ ├── args.js # 参数解析
|
||||||
|
│ ├── inject.js # 核心注入功能
|
||||||
|
│ ├── utils.js # 工具函数
|
||||||
|
│ └── help.js # 帮助信息
|
||||||
|
├── scripts/
|
||||||
|
│ └── dev-monitor.js # 监控脚本
|
||||||
|
├── package.json # 项目配置
|
||||||
|
└── README.md # 说明文档
|
||||||
|
```
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
- ✅ Node.js >= 14.0.0
|
||||||
|
- ✅ 所有现代浏览器
|
||||||
|
- ✅ React、Vue、Angular、Next.js 等
|
||||||
|
- ✅ Vite、Webpack、Create React App 等构建工具
|
||||||
|
|
||||||
|
## 📦 发布配置
|
||||||
|
|
||||||
|
本项目已配置 npm publish 时仅包含必要的文件:
|
||||||
|
|
||||||
|
**包含的文件:**
|
||||||
|
- ✅ `bin/` - CLI 可执行文件
|
||||||
|
- ✅ `lib/` - 核心库文件
|
||||||
|
- ✅ `scripts/` - 监控脚本
|
||||||
|
- ✅ `*.md` - 文档文件
|
||||||
|
- ✅ `LICENSE` - 许可证
|
||||||
|
|
||||||
|
**排除的文件:**
|
||||||
|
- ❌ `test/` - 测试文件
|
||||||
|
- ❌ `test-projects/` - 测试项目
|
||||||
|
- ❌ 所有 `*.test.js` 文件
|
||||||
|
- ❌ 测试和演示 HTML 文件
|
||||||
|
- ❌ 日志和缓存文件
|
||||||
|
- ❌ 开发工具配置
|
||||||
|
|
||||||
|
完整配置请查看 `package.json` 的 `files` 字段和 `.npmignore` 文件。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **绝对路径**:确保静态文件服务器可以访问该路径
|
||||||
|
2. **重复注入**:工具会自动处理重复注入问题
|
||||||
|
3. **权限**:确保有 HTML 文件的写入权限
|
||||||
|
4. **备份**:建议在操作前备份重要文件
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
54
qiming-dev-inject/bin/index.js
Normal file
54
qiming-dev-inject/bin/index.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { parseArgs } from '../lib/args.js';
|
||||||
|
import { installScript } from '../lib/inject.js';
|
||||||
|
import { uninstallScript } from '../lib/inject.js';
|
||||||
|
import { showHelp, showVersion } from '../lib/help.js';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
// 显示帮助信息
|
||||||
|
if (args.includes('--help') || args.includes('-h')) {
|
||||||
|
showHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示版本信息
|
||||||
|
if (args.includes('--version') || args.includes('-v')) {
|
||||||
|
showVersion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseArgs(args);
|
||||||
|
|
||||||
|
if (!parsed.command) {
|
||||||
|
console.error('❌ 错误:请指定命令 (install 或 uninstall)');
|
||||||
|
console.log('使用 --help 查看帮助信息');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (parsed.command) {
|
||||||
|
case 'install':
|
||||||
|
await installScript(parsed.options);
|
||||||
|
break;
|
||||||
|
case 'uninstall':
|
||||||
|
await uninstallScript(parsed.options);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`❌ 未知命令: ${parsed.command}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 执行失败:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
48
qiming-dev-inject/lib/args.js
Normal file
48
qiming-dev-inject/lib/args.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
export function parseArgs(args) {
|
||||||
|
const result = {
|
||||||
|
command: null,
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析命令
|
||||||
|
if (args[0] === 'install' || args[0] === 'uninstall') {
|
||||||
|
result.command = args[0];
|
||||||
|
args = args.slice(1);
|
||||||
|
} else if (args.length === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析选项
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
|
||||||
|
if (arg.startsWith('--remote=')) {
|
||||||
|
result.options.remote = arg.split('=')[1];
|
||||||
|
} else if (arg === '--remote' && i + 1 < args.length) {
|
||||||
|
result.options.remote = args[i + 1];
|
||||||
|
i++; // 跳过下一个参数
|
||||||
|
} else if (arg.startsWith('--file=')) {
|
||||||
|
result.options.file = arg.split('=')[1];
|
||||||
|
} else if (arg === '--file' && i + 1 < args.length) {
|
||||||
|
result.options.file = args[i + 1];
|
||||||
|
i++; // 跳过下一个参数
|
||||||
|
} else if (arg === '--dry-run') {
|
||||||
|
result.options.dryRun = true;
|
||||||
|
} else if (arg === '--verbose') {
|
||||||
|
result.options.verbose = true;
|
||||||
|
} else if (arg === '--framework' || arg === '-f') {
|
||||||
|
result.options.framework = true;
|
||||||
|
} else if (!arg.startsWith('--')) {
|
||||||
|
// 未知参数,可能是错误输入
|
||||||
|
console.warn(`⚠️ 忽略未知参数: ${arg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为 install 命令设置 remote 默认值
|
||||||
|
if (result.command === 'install' && !result.options.remote) {
|
||||||
|
result.options.remote = '/sdk/dev-monitor.js';
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
112
qiming-dev-inject/lib/code-cleaner.js
Normal file
112
qiming-dev-inject/lib/code-cleaner.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* 代码清理工具类 - 清理多余的空白字符和逗号
|
||||||
|
*/
|
||||||
|
export class CodeCleaner {
|
||||||
|
/**
|
||||||
|
* 清理多余的空白行
|
||||||
|
* @param {string} content - 要清理的内容
|
||||||
|
* @param {boolean} preserveDoubleNewline - 是否保留双换行(默认 true)
|
||||||
|
* @returns {string} 清理后的内容
|
||||||
|
*/
|
||||||
|
static cleanBlankLines(content, preserveDoubleNewline = true) {
|
||||||
|
if (preserveDoubleNewline) {
|
||||||
|
// 保留单行空白,清理多余的连续空白行
|
||||||
|
return content.replace(/\n\s*\n\s*\n+/g, '\n\n');
|
||||||
|
} else {
|
||||||
|
// 清理所有多余空白行,只保留单个换行
|
||||||
|
return content.replace(/\n\s*\n+/g, '\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理行尾的空白字符
|
||||||
|
* @param {string} content - 要清理的内容
|
||||||
|
* @returns {string} 清理后的内容
|
||||||
|
*/
|
||||||
|
static cleanTrailingSpaces(content) {
|
||||||
|
return content.replace(/[ \t]+$/gm, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理多余的逗号
|
||||||
|
* @param {string} content - 要清理的内容
|
||||||
|
* @returns {string} 清理后的内容
|
||||||
|
*/
|
||||||
|
static cleanCommas(content) {
|
||||||
|
// 1. 清理单独的逗号行(整行都是逗号和空白)
|
||||||
|
content = content.replace(/^\s*,\s*\n/gm, '');
|
||||||
|
// 2. 清理行首的逗号和空白
|
||||||
|
content = content.replace(/^\s*,\s+/gm, '');
|
||||||
|
// 3. 清理连续的多个逗号
|
||||||
|
content = content.replace(/,\s*,\s*/g, ',');
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理空的标签对
|
||||||
|
* @param {string} content - 要清理的内容
|
||||||
|
* @param {string} tagName - 标签名
|
||||||
|
* @returns {string} 清理后的内容
|
||||||
|
*/
|
||||||
|
static cleanEmptyTags(content, tagName) {
|
||||||
|
const regex = new RegExp(`<${tagName}[^>]*>\\s*</${tagName}>`, 'g');
|
||||||
|
return content.replace(regex, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理标签中间的空白行
|
||||||
|
* @param {string} content - 要清理的内容
|
||||||
|
* @param {string} tagName - 标签名(可以是正则表达式)
|
||||||
|
* @returns {string} 清理后的内容
|
||||||
|
*/
|
||||||
|
static cleanTagWhitespace(content, tagName) {
|
||||||
|
// 清理如 <Head>\n\n\n</Head> -> <Head>\n</Head>
|
||||||
|
content = content.replace(
|
||||||
|
new RegExp(`(<${tagName}[^>]*>)\\s*\\n\\s*\\n(\\s*</${tagName}>)`, 'g'),
|
||||||
|
'$1\n$2'
|
||||||
|
);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 综合清理:清理所有常见的代码格式问题
|
||||||
|
* @param {string} content - 要清理的内容
|
||||||
|
* @param {object} options - 清理选项
|
||||||
|
* @returns {string} 清理后的内容
|
||||||
|
*/
|
||||||
|
static cleanAll(content, options = {}) {
|
||||||
|
const {
|
||||||
|
blankLines = true,
|
||||||
|
trailingSpaces = true,
|
||||||
|
commas = false,
|
||||||
|
emptyTags = true,
|
||||||
|
tagWhitespace = true
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
if (blankLines) {
|
||||||
|
content = this.cleanBlankLines(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trailingSpaces) {
|
||||||
|
content = this.cleanTrailingSpaces(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commas) {
|
||||||
|
content = this.cleanCommas(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emptyTags) {
|
||||||
|
['head', 'Head'].forEach(tag => {
|
||||||
|
content = this.cleanEmptyTags(content, tag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagWhitespace) {
|
||||||
|
['Head', 'head'].forEach(tag => {
|
||||||
|
content = this.cleanTagWhitespace(content, tag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
133
qiming-dev-inject/lib/file-finder.js
Normal file
133
qiming-dev-inject/lib/file-finder.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { lookupFiles } from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件查找工具类 - 支持多种后缀名尝试机制
|
||||||
|
*/
|
||||||
|
export class FileFinder {
|
||||||
|
/**
|
||||||
|
* 查找具有多种后缀名的文件
|
||||||
|
* @param {string} basePath - 基础路径(不包含后缀)
|
||||||
|
* @param {string[]} extensions - 后缀名数组,按优先级排序
|
||||||
|
* @param {string} startDir - 起始目录
|
||||||
|
* @returns {string|null} 找到的文件路径或 null
|
||||||
|
*/
|
||||||
|
static findWithExtensions(basePath, extensions = ['.tsx', '.ts', '.jsx', '.js'], startDir = process.cwd()) {
|
||||||
|
for (const ext of extensions) {
|
||||||
|
const fullPath = path.join(startDir, basePath + ext);
|
||||||
|
if (fs.existsSync(fullPath)) {
|
||||||
|
return basePath + ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找多个候选文件
|
||||||
|
* @param {string[]} candidates - 候选文件路径数组
|
||||||
|
* @param {string} startDir - 起始目录
|
||||||
|
* @returns {string[]} 找到的文件路径数组
|
||||||
|
*/
|
||||||
|
static findMultiple(candidates, startDir = process.cwd()) {
|
||||||
|
const foundFiles = [];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (candidate === '*.html') {
|
||||||
|
// 特殊处理:查找所有 HTML 文件
|
||||||
|
const htmlFiles = lookupFiles(startDir);
|
||||||
|
foundFiles.push(...htmlFiles);
|
||||||
|
} else if (fs.existsSync(path.join(startDir, candidate))) {
|
||||||
|
foundFiles.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找框架特定的入口文件
|
||||||
|
* @param {string} projectType - 项目类型
|
||||||
|
* @param {string} startDir - 起始目录
|
||||||
|
* @returns {string[]} 找到的文件路径数组
|
||||||
|
*/
|
||||||
|
static findFrameworkEntry(projectType, startDir = process.cwd()) {
|
||||||
|
const foundFiles = [];
|
||||||
|
|
||||||
|
switch (projectType) {
|
||||||
|
case 'vite':
|
||||||
|
// 尝试查找 vite.config 文件
|
||||||
|
const viteConfig = this.findWithExtensions('vite.config', ['.js', '.ts', '.mjs'], startDir);
|
||||||
|
if (viteConfig) {
|
||||||
|
foundFiles.push(viteConfig);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'next-pages':
|
||||||
|
// 尝试查找 _document 文件
|
||||||
|
const documentFile = this.findWithExtensions('pages/_document', ['.tsx', '.jsx', '.ts', '.js'], startDir);
|
||||||
|
if (documentFile) {
|
||||||
|
foundFiles.push(documentFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试查找 _app 文件
|
||||||
|
const appFile = this.findWithExtensions('pages/_app', ['.tsx', '.jsx', '.ts', '.js'], startDir);
|
||||||
|
if (appFile) {
|
||||||
|
foundFiles.push(appFile);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'next-app':
|
||||||
|
// 尝试查找 layout 文件
|
||||||
|
const layoutFile = this.findWithExtensions('app/layout', ['.tsx', '.jsx', '.ts', '.js'], startDir);
|
||||||
|
if (layoutFile) {
|
||||||
|
foundFiles.push(layoutFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试查找 globals.css
|
||||||
|
const globalsCss = 'app/globals.css';
|
||||||
|
if (fs.existsSync(path.join(startDir, globalsCss))) {
|
||||||
|
foundFiles.push(globalsCss);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'next-hybrid':
|
||||||
|
// 尝试查找 app/layout 文件
|
||||||
|
const appLayoutFile = this.findWithExtensions('app/layout', ['.tsx', '.jsx', '.ts', '.js'], startDir);
|
||||||
|
if (appLayoutFile) {
|
||||||
|
foundFiles.push(appLayoutFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试查找 pages/_document 文件
|
||||||
|
const pagesDocumentFile = this.findWithExtensions('pages/_document', ['.tsx', '.jsx', '.ts', '.js'], startDir);
|
||||||
|
if (pagesDocumentFile) {
|
||||||
|
foundFiles.push(pagesDocumentFile);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create-react-app':
|
||||||
|
// 尝试查找 HTML 文件
|
||||||
|
const htmlFiles = ['public/index.html', 'index.html'];
|
||||||
|
foundFiles.push(...this.findMultiple(htmlFiles, startDir));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'html-static':
|
||||||
|
// 尝试查找 HTML 文件
|
||||||
|
const staticHtmlFiles = ['index.html', '*.html'];
|
||||||
|
foundFiles.push(...this.findMultiple(staticHtmlFiles, startDir));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 默认查找 HTML 文件
|
||||||
|
const defaultHtmlFiles = ['index.html', '*.html'];
|
||||||
|
foundFiles.push(...this.findMultiple(defaultHtmlFiles, startDir));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundFiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出便捷方法
|
||||||
|
export function findFrameworkEntry(projectType, startDir = process.cwd()) {
|
||||||
|
return FileFinder.findFrameworkEntry(projectType, startDir);
|
||||||
|
}
|
||||||
|
|
||||||
850
qiming-dev-inject/lib/framework-inject.js
Normal file
850
qiming-dev-inject/lib/framework-inject.js
Normal file
@@ -0,0 +1,850 @@
|
|||||||
|
import {
|
||||||
|
readFile,
|
||||||
|
writeFile,
|
||||||
|
parseRemoteType,
|
||||||
|
generateScriptTag,
|
||||||
|
injectScriptToHtml,
|
||||||
|
removeInjectedScripts,
|
||||||
|
log,
|
||||||
|
logInfo,
|
||||||
|
logSuccess,
|
||||||
|
logError
|
||||||
|
} from './utils.js';
|
||||||
|
import { FileFinder } from './file-finder.js';
|
||||||
|
import { CodeCleaner } from './code-cleaner.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// 检测项目类型
|
||||||
|
export function detectProjectType(startDir = process.cwd()) {
|
||||||
|
const indicators = {
|
||||||
|
'vite': ['vite.config.js', 'vite.config.ts'],
|
||||||
|
'next-pages': ['next.config.js', 'pages/'],
|
||||||
|
'next-app': ['next.config.js', 'app/'],
|
||||||
|
'create-react-app': ['public/index.html', 'src/'],
|
||||||
|
'webpack': ['webpack.config.js'],
|
||||||
|
'html-static': ['index.html', '.html']
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 按优先级顺序检测
|
||||||
|
const priorityOrder = ['vite', 'next-app', 'next-pages', 'create-react-app', 'webpack', 'html-static'];
|
||||||
|
|
||||||
|
for (const type of priorityOrder) {
|
||||||
|
const files = indicators[type];
|
||||||
|
for (const file of files) {
|
||||||
|
if (fs.existsSync(path.join(startDir, file))) {
|
||||||
|
// 特殊处理 Next.js
|
||||||
|
if (type.startsWith('next-')) {
|
||||||
|
const hasPages = fs.existsSync(path.join(startDir, 'pages'));
|
||||||
|
const hasApp = fs.existsSync(path.join(startDir, 'app'));
|
||||||
|
|
||||||
|
if (hasApp && hasPages) {
|
||||||
|
return 'next-hybrid'; // 两个目录都存在
|
||||||
|
} else if (hasApp) {
|
||||||
|
return 'next-app';
|
||||||
|
} else if (hasPages) {
|
||||||
|
return 'next-pages';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logError(`项目类型检测失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找框架特定的入口文件 - 支持多次尝试机制
|
||||||
|
export function findFrameworkEntry(projectType, startDir = process.cwd()) {
|
||||||
|
return FileFinder.findFrameworkEntry(projectType, startDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为 Vite 项目注入脚本插件
|
||||||
|
export function injectToViteConfig(filePath, remote, scriptTag) {
|
||||||
|
try {
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!fs.existsSync(path.join(process.cwd(), filePath))) {
|
||||||
|
log(`Vite 配置文件 ${filePath} 不存在,跳过`, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = readFile(filePath);
|
||||||
|
|
||||||
|
// 先移除之前的注入(如果存在)- 支持新旧两种格式
|
||||||
|
// 使用字符串查找方式,确保能移除所有注入块(包括多个重复的情况)
|
||||||
|
const startMarker = '// <!-- DEV-INJECT-START -->';
|
||||||
|
const endMarker = '// <!-- DEV-INJECT-END -->';
|
||||||
|
|
||||||
|
let startIndex = content.indexOf(startMarker);
|
||||||
|
while (startIndex !== -1) {
|
||||||
|
const endIndex = content.indexOf(endMarker, startIndex);
|
||||||
|
if (endIndex !== -1) {
|
||||||
|
// 找到结束标记,移除从开始标记到结束标记后的所有内容
|
||||||
|
// 注意:逗号在 })() 和注释结束之间,所以会被包含在移除范围内
|
||||||
|
let removeEnd = endIndex + endMarker.length;
|
||||||
|
// 移除结束标记后的空白和换行
|
||||||
|
while (removeEnd < content.length && (content[removeEnd] === ' ' || content[removeEnd] === '\t' || content[removeEnd] === '\n')) {
|
||||||
|
removeEnd++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除整个块:从开始标记(包括前面的换行和空白)到结束标记后
|
||||||
|
// 向前查找,移除开始标记前的换行和空白
|
||||||
|
let removeStart = startIndex;
|
||||||
|
while (removeStart > 0 && (content[removeStart - 1] === ' ' || content[removeStart - 1] === '\t' || content[removeStart - 1] === '\n')) {
|
||||||
|
removeStart--;
|
||||||
|
if (content[removeStart] === '\n') {
|
||||||
|
removeStart++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content = content.slice(0, removeStart) + content.slice(removeEnd);
|
||||||
|
} else {
|
||||||
|
// 没有找到结束标记,只移除开始标记
|
||||||
|
content = content.slice(0, startIndex) + content.slice(startIndex + startMarker.length);
|
||||||
|
}
|
||||||
|
// 继续查找下一个注入块
|
||||||
|
startIndex = content.indexOf(startMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除旧版格式(向后兼容)
|
||||||
|
content = content.replace(
|
||||||
|
/\/\/\s*dev-inject-plugin[\s\S]*?}\s*}\s*,?\s*/g,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
// 清理多余的逗号和空行
|
||||||
|
// 移除连续的逗号
|
||||||
|
content = content.replace(/,\s*,/g, ',');
|
||||||
|
// 移除数组开头的逗号
|
||||||
|
content = content.replace(/\[\s*,/g, '[');
|
||||||
|
// 移除数组元素之间多余的逗号
|
||||||
|
content = content.replace(/,\s*\)/g, ')');
|
||||||
|
content = content.replace(/,\s*\n\s*\]/g, '\n ]');
|
||||||
|
content = content.replace(/,\s*\n\s*\[/g, ' [');
|
||||||
|
// 清理多余的空行(保留最多一个空行)
|
||||||
|
content = content.replace(/\n\s*\n\s*\n+/g, '\n\n');
|
||||||
|
|
||||||
|
// 生成插件代码,传入原始 remote URL 而不是 scriptTag
|
||||||
|
const pluginCode = generateVitePlugin(remote, scriptTag);
|
||||||
|
|
||||||
|
// 查找 plugins 数组,并在其中插入插件
|
||||||
|
const pluginsMatch = content.match(/(plugins:\s*\[)([\s\S]*?)(\])/);
|
||||||
|
if (pluginsMatch) {
|
||||||
|
const beforePlugins = pluginsMatch[1];
|
||||||
|
const pluginsContent = pluginsMatch[2].trim();
|
||||||
|
const afterPlugins = pluginsMatch[3];
|
||||||
|
|
||||||
|
// 插入插件到 plugins 数组开头
|
||||||
|
const newPlugins = beforePlugins + '\n ' + pluginCode + '\n ' + pluginsContent + '\n ' + afterPlugins;
|
||||||
|
content = content.replace(pluginsMatch[0], newPlugins);
|
||||||
|
} else {
|
||||||
|
// 如果没有 plugins 数组,需要创建
|
||||||
|
log(`未找到 plugins 配置,Vite 配置可能不标准: ${filePath}`, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(filePath, content);
|
||||||
|
logSuccess(`已向 ${filePath} 注入 Vite 插件`);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log(`Vite 插件注入失败: ${error.message}`, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 更规范与健壮的版本
|
||||||
|
function generateVitePlugin(remote, { insertPosition = '</head>' } = {}) {
|
||||||
|
const safeRemote = JSON.stringify(remote); // 避免注入问题
|
||||||
|
const pluginName = 'dev-inject';
|
||||||
|
const scriptId = 'dev-inject-monitor';
|
||||||
|
|
||||||
|
const scriptInjection = `
|
||||||
|
<script data-id="${scriptId}">
|
||||||
|
(function() {
|
||||||
|
const remote = ${safeRemote};
|
||||||
|
const separator = remote.includes('?') ? '&' : '?';
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = remote + separator + 't=' + Date.now();
|
||||||
|
script.dataset.id = '${scriptId}-script';
|
||||||
|
script.defer = true;
|
||||||
|
// 防止重复注入
|
||||||
|
if (!document.querySelector('[data-id="${scriptId}-script"]')) {
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
// <!-- DEV-INJECT-START -->
|
||||||
|
{
|
||||||
|
name: '${pluginName}',
|
||||||
|
enforce: 'post', // 确保在 HTML 注入阶段最后执行
|
||||||
|
transformIndexHtml(html) {
|
||||||
|
if (!html.includes('data-id="${scriptId}"')) {
|
||||||
|
return html.replace(${JSON.stringify(insertPosition)}, \`${scriptInjection}\\n${insertPosition}\`);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// <!-- DEV-INJECT-END -->
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 为 Next.js 注入脚本到 _document
|
||||||
|
export function injectToNextDocument(filePath, scriptTag, remote = null) {
|
||||||
|
try {
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!fs.existsSync(path.join(process.cwd(), filePath))) {
|
||||||
|
log(`Next.js 文件 ${filePath} 不存在,跳过`, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = readFile(filePath);
|
||||||
|
|
||||||
|
// 检查是否是 _document.tsx/_document.js
|
||||||
|
if (filePath.includes('_document')) {
|
||||||
|
return injectToDocumentComponent(content, scriptTag, filePath, remote);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是 _app.tsx/_app.js
|
||||||
|
if (filePath.includes('_app')) {
|
||||||
|
return injectToAppComponent(content, scriptTag, filePath, remote);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`无法识别的 Next.js 文件类型: ${filePath}`, true);
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log(`Next.js 注入失败: ${error.message}`, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注入到 Next.js _document 组件
|
||||||
|
function injectToDocumentComponent(content, scriptTag, filePath, remote = null) {
|
||||||
|
const isTypeScript = filePath.endsWith('.tsx');
|
||||||
|
|
||||||
|
// 先移除之前的注入
|
||||||
|
content = removeNextDocumentInjection(content);
|
||||||
|
|
||||||
|
// 生成脚本加载代码(优先使用动态方式)
|
||||||
|
let scriptInjection;
|
||||||
|
if (remote) {
|
||||||
|
// 使用动态脚本加载器(推荐方式,防止缓存)
|
||||||
|
const scriptLoader = generateDynamicScriptLoader(remote);
|
||||||
|
scriptInjection = `{/* DEV-INJECT-START */}
|
||||||
|
{typeof window !== 'undefined' && (
|
||||||
|
<script dangerouslySetInnerHTML={{
|
||||||
|
__html: ${JSON.stringify(scriptLoader.trim())}
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
{/* DEV-INJECT-END */}`;
|
||||||
|
} else {
|
||||||
|
// 向后兼容:使用旧的 scriptTag 方式
|
||||||
|
scriptInjection = `{/* DEV-INJECT-START */}
|
||||||
|
{typeof window !== 'undefined' && (
|
||||||
|
${scriptTag}
|
||||||
|
)}
|
||||||
|
{/* DEV-INJECT-END */}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找 <Head> 组件
|
||||||
|
const headRegex = /<Head[^>]*>([\s\S]*?)<\/Head>/;
|
||||||
|
const headMatch = content.match(headRegex);
|
||||||
|
|
||||||
|
if (headMatch) {
|
||||||
|
// 在 <Head> 组件内注入 - 使用 DEV-INJECT-START/END 标识
|
||||||
|
const beforeHead = headMatch[1];
|
||||||
|
const newHead = beforeHead + '\n ' + scriptInjection;
|
||||||
|
// 修复多余的 < 符号问题
|
||||||
|
const headAttributes = headMatch[0].match(/<Head([^>]*)>/);
|
||||||
|
const attributes = headAttributes ? headAttributes[1] : '';
|
||||||
|
content = content.replace(headRegex, `<Head${attributes}>\n ${newHead}\n </Head>`);
|
||||||
|
} else {
|
||||||
|
// 如果没有 <Head> 组件,需要添加
|
||||||
|
const importReactMatch = content.match(/import.*React.*from.*react/);
|
||||||
|
if (importReactMatch) {
|
||||||
|
const afterImport = content.indexOf(importReactMatch[0]) + importReactMatch[0].length;
|
||||||
|
const headComponent = isTypeScript ? `
|
||||||
|
import Head from 'next/head';` : `
|
||||||
|
import Head from 'next/head';`;
|
||||||
|
|
||||||
|
content = content.slice(0, afterImport) + headComponent + content.slice(afterImport);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 Document 组件的 render 方法中添加 Head
|
||||||
|
const renderMatch = content.match(/render\(\)\s*{[\s\S]*?return\s*\(/);
|
||||||
|
if (renderMatch) {
|
||||||
|
const insertPoint = content.indexOf(renderMatch[0]) + renderMatch[0].length;
|
||||||
|
let headInsert;
|
||||||
|
if (remote) {
|
||||||
|
// 使用动态脚本加载器
|
||||||
|
const scriptLoader = generateDynamicScriptLoader(remote);
|
||||||
|
headInsert = `
|
||||||
|
<Head>
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
{typeof window !== 'undefined' && (
|
||||||
|
<script dangerouslySetInnerHTML={{
|
||||||
|
__html: ${JSON.stringify(scriptLoader.trim())}
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
</Head>`;
|
||||||
|
} else {
|
||||||
|
// 向后兼容:使用旧的 scriptTag 方式
|
||||||
|
headInsert = `
|
||||||
|
<Head>
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
{typeof window !== 'undefined' && ${scriptTag.replace(/<script[^>]*>|<\/script>/g, '')}}
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
</Head>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = content.slice(0, insertPoint) + headInsert + content.slice(insertPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(filePath, content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注入到 Next.js _app 组件
|
||||||
|
function injectToAppComponent(content, scriptTag, filePath, remote = null) {
|
||||||
|
// 移除之前的注入
|
||||||
|
content = removeNextAppInjection(content);
|
||||||
|
|
||||||
|
// 在 _app 组件中注入,使用 useEffect
|
||||||
|
const scriptLoader = generateScriptLoader(scriptTag, false, remote);
|
||||||
|
const useEffectInsert = `
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
${scriptLoader}
|
||||||
|
{/* DEV-INJECT-END */}`;
|
||||||
|
|
||||||
|
// 查找现有的 useEffect 或组件末尾
|
||||||
|
const existingUseEffect = content.match(/useEffect\([^)]*\)\s*=>\s*{[\s\S]*?},\s*\[?\s*\]?\s*\);?/);
|
||||||
|
|
||||||
|
if (existingUseEffect) {
|
||||||
|
// 在现有 useEffect 中添加
|
||||||
|
content = content.replace(
|
||||||
|
existingUseEffect[0],
|
||||||
|
existingUseEffect[0].replace('}, []);', ' \n' + useEffectInsert.replace(/useEffect\([^)]*\)\s*=>\s*{\n/, '').replace(/}, \[\];/, '') + '\n }, [];')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 添加新的 useEffect
|
||||||
|
const componentEndMatch = content.match(/export default.*|}\s*$/);
|
||||||
|
if (componentEndMatch) {
|
||||||
|
const insertPoint = content.lastIndexOf(componentEndMatch[0]);
|
||||||
|
content = content.slice(0, insertPoint) + useEffectInsert + '\n\n' + content.slice(insertPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(filePath, content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为 Next.js App Router 注入
|
||||||
|
export function injectToNextAppLayout(filePath, scriptTag, remote = null) {
|
||||||
|
try {
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!fs.existsSync(path.join(process.cwd(), filePath))) {
|
||||||
|
log(`Next.js App Router 文件 ${filePath} 不存在,跳过`, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = readFile(filePath);
|
||||||
|
|
||||||
|
// 移除之前的注入
|
||||||
|
content = removeNextAppLayoutInjection(content);
|
||||||
|
|
||||||
|
// 在 layout.tsx/js 中注入到 <head> 内
|
||||||
|
const scriptLoader = generateScriptLoader(scriptTag, true, remote);
|
||||||
|
|
||||||
|
// 查找 <head> 标签
|
||||||
|
const headRegex = /<head[^>]*>([\s\S]*?)<\/head>/i;
|
||||||
|
const headMatch = content.match(headRegex);
|
||||||
|
|
||||||
|
if (headMatch) {
|
||||||
|
// 在 <head> 内注入
|
||||||
|
const beforeHead = headMatch[1];
|
||||||
|
const newHead = beforeHead + '\n ' + scriptLoader;
|
||||||
|
content = content.replace(headRegex, `<head${headMatch[0].slice(5, -6)}${newHead}</head>`);
|
||||||
|
} else {
|
||||||
|
// 如果没有 <head> 标签,在 <html> 标签后添加
|
||||||
|
const htmlRegex = /<html[^>]*>/;
|
||||||
|
const htmlMatch = content.match(htmlRegex);
|
||||||
|
if (htmlMatch) {
|
||||||
|
const insertPoint = content.indexOf(htmlMatch[0]) + htmlMatch[0].length;
|
||||||
|
const headTag = '\n <head>\n ' + scriptLoader + '\n </head>';
|
||||||
|
content = content.slice(0, insertPoint) + headTag + content.slice(insertPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(filePath, content);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
log(`Next.js App Router 注入失败: ${error.message}`, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成动态脚本加载器 - 使用运行时时间戳防止缓存
|
||||||
|
function generateDynamicScriptLoader(remote, scriptId = 'dev-inject-monitor') {
|
||||||
|
const safeRemote = JSON.stringify(remote); // 避免注入问题
|
||||||
|
return `
|
||||||
|
(function() {
|
||||||
|
const remote = ${safeRemote};
|
||||||
|
const separator = remote.includes('?') ? '&' : '?';
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = remote + separator + 't=' + Date.now();
|
||||||
|
script.dataset.id = '${scriptId}-script';
|
||||||
|
script.defer = true;
|
||||||
|
// 防止重复注入
|
||||||
|
if (!document.querySelector('[data-id="${scriptId}-script"]')) {
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成脚本加载器 - 使用 DEV-INJECT 标识和动态时间戳
|
||||||
|
function generateScriptLoader(scriptTag, isHead = false, remote = null) {
|
||||||
|
// 如果提供了 remote URL,使用动态脚本加载器(推荐方式)
|
||||||
|
if (remote) {
|
||||||
|
const scriptLoader = generateDynamicScriptLoader(remote);
|
||||||
|
|
||||||
|
if (isHead) {
|
||||||
|
// 对于 App Router layout,注入到 <head> 内
|
||||||
|
return `{/* DEV-INJECT-START */}
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<script dangerouslySetInnerHTML={{
|
||||||
|
__html: ${JSON.stringify(scriptLoader.trim())}
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
{/* DEV-INJECT-END */}`;
|
||||||
|
}
|
||||||
|
// useEffect 方式(用于 _app 组件)
|
||||||
|
return `useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
${scriptLoader.trim()}
|
||||||
|
}
|
||||||
|
}, []);`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向后兼容:如果没有提供 remote,使用旧的 scriptTag 方式
|
||||||
|
if (isHead) {
|
||||||
|
// 对于 App Router layout,注入到 <head> 内
|
||||||
|
return `{/* DEV-INJECT-START */}
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
${scriptTag}
|
||||||
|
)}
|
||||||
|
{/* DEV-INJECT-END */}`;
|
||||||
|
}
|
||||||
|
// 旧版 useEffect 方式(保留向后兼容)
|
||||||
|
return `useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
${scriptTag.replace(/<script[^>]*>|<\/script>/g, '').trim()}
|
||||||
|
}
|
||||||
|
}, []);`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用的移除注入内容函数
|
||||||
|
function removeInjectionContent(content, startMarker = '/* DEV-INJECT-START */', endMarker = '/* DEV-INJECT-END */') {
|
||||||
|
let startIndex = content.indexOf(startMarker);
|
||||||
|
while (startIndex !== -1) {
|
||||||
|
const endIndex = content.indexOf(endMarker, startIndex);
|
||||||
|
|
||||||
|
if (endIndex !== -1) {
|
||||||
|
// 找到这两行前后的换行符
|
||||||
|
const startLineIndex = content.lastIndexOf('\n', startIndex - 1);
|
||||||
|
const endLineEnd = content.indexOf('\n', endIndex + endMarker.length);
|
||||||
|
|
||||||
|
if (startLineIndex !== -1 && endLineEnd !== -1) {
|
||||||
|
// 移除整块内容
|
||||||
|
content = content.slice(0, startLineIndex + 1) + content.slice(endLineEnd + 1);
|
||||||
|
} else if (startIndex >= 0 && endIndex >= startIndex) {
|
||||||
|
// 简单的字符串切片
|
||||||
|
const endPos = endIndex + endMarker.length;
|
||||||
|
const endLine = content.indexOf('\n', endPos);
|
||||||
|
if (endLine !== -1) {
|
||||||
|
content = content.slice(0, startIndex) + content.slice(endLine + 1);
|
||||||
|
} else {
|
||||||
|
content = content.slice(0, startIndex) + content.slice(endIndex + endMarker.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续查找下一个
|
||||||
|
startIndex = content.indexOf(startMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理多余的空白行
|
||||||
|
content = content.replace(/\n\s*\n\s*\n+/g, '\n\n');
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 Next.js Document 注入
|
||||||
|
function removeNextDocumentInjection(content) {
|
||||||
|
return removeInjectionContent(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 Next.js App 注入
|
||||||
|
function removeNextAppInjection(content) {
|
||||||
|
// 移除 DEV-INJECT 相关的 useEffect(只保留新版标准标识)
|
||||||
|
const patterns = [
|
||||||
|
/\{\/\*\s*DEV-INJECT-START\s*\/\*\/[\s\S]*?\{\/\*\s*DEV-INJECT-END\s*\/\*\/\}/g,
|
||||||
|
/useEffect\(\(\) => \{\s*if \(typeof window !== 'undefined'\) \{[\s\S]*?\}\s*\}, \[\]\);?/g
|
||||||
|
];
|
||||||
|
|
||||||
|
patterns.forEach(pattern => {
|
||||||
|
content = content.replace(pattern, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 Next.js App Layout 注入
|
||||||
|
function removeNextAppLayoutInjection(content) {
|
||||||
|
// 使用字符串匹配移除 DEV-INJECT-START/END 之间的内容
|
||||||
|
const startMarker = '/* DEV-INJECT-START */';
|
||||||
|
const endMarker = '/* DEV-INJECT-END */';
|
||||||
|
|
||||||
|
let startIndex = content.indexOf(startMarker);
|
||||||
|
while (startIndex !== -1) {
|
||||||
|
const endIndex = content.indexOf(endMarker, startIndex);
|
||||||
|
|
||||||
|
if (endIndex !== -1) {
|
||||||
|
// 找到这两行前后的换行符
|
||||||
|
const startLineIndex = content.lastIndexOf('\n', startIndex - 1);
|
||||||
|
const endLineEnd = content.indexOf('\n', endIndex + endMarker.length);
|
||||||
|
|
||||||
|
if (startLineIndex !== -1 && endLineEnd !== -1) {
|
||||||
|
// 移除整块内容
|
||||||
|
content = content.slice(0, startLineIndex + 1) + content.slice(endLineEnd + 1);
|
||||||
|
} else {
|
||||||
|
const endPos = endIndex + endMarker.length;
|
||||||
|
const endLine = content.indexOf('\n', endPos);
|
||||||
|
if (endLine !== -1) {
|
||||||
|
content = content.slice(0, startIndex) + content.slice(endLine + 1);
|
||||||
|
} else {
|
||||||
|
content = content.slice(0, startIndex) + content.slice(endIndex + endMarker.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startIndex = content.indexOf(startMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 CodeCleaner 清理代码格式
|
||||||
|
content = CodeCleaner.cleanAll(content, {
|
||||||
|
blankLines: true,
|
||||||
|
trailingSpaces: true,
|
||||||
|
emptyTags: true,
|
||||||
|
tagWhitespace: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 Vite 插件注入
|
||||||
|
function removeViteInjection(content) {
|
||||||
|
// 移除新版标准标识的注入(优先),包括后面的逗号
|
||||||
|
content = content.replace(
|
||||||
|
/\/\/ <!-- DEV-INJECT-START -->[\s\S]*?\/\/ <!-- DEV-INJECT-END -->\s*,?\s*/g,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
// 移除旧版格式的注入(向后兼容)
|
||||||
|
content = content.replace(
|
||||||
|
/\/\/ dev-inject-plugin[\s\S]*?}\s*}\s*,?\s*/g,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
// 使用 CodeCleaner 清理代码格式
|
||||||
|
content = CodeCleaner.cleanAll(content, {
|
||||||
|
blankLines: true,
|
||||||
|
commas: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能卸载 - 智能移除不同框架的注入
|
||||||
|
export function smartUninstall(options = {}) {
|
||||||
|
const { file, dryRun = false, verbose = false } = options;
|
||||||
|
|
||||||
|
logInfo('开始移除注入的脚本...');
|
||||||
|
|
||||||
|
// 检测项目类型
|
||||||
|
const projectType = detectProjectType();
|
||||||
|
// 只在 verbose 模式下输出检测到的项目类型(避免重复)
|
||||||
|
log(`检测到项目类型: ${projectType}`, verbose);
|
||||||
|
|
||||||
|
// 查找入口文件
|
||||||
|
const entryFiles = file ? [file] : findFrameworkEntry(projectType);
|
||||||
|
|
||||||
|
if (entryFiles.length === 0) {
|
||||||
|
logInfo('未找到需要处理的文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`找到 ${entryFiles.length} 个文件:`, verbose);
|
||||||
|
entryFiles.forEach(f => log(` - ${f}`, verbose));
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
|
||||||
|
for (const entryFile of entryFiles) {
|
||||||
|
try {
|
||||||
|
if (projectType === 'next-pages' || projectType === 'next-hybrid') {
|
||||||
|
if (entryFile.includes('_document')) {
|
||||||
|
successCount += uninstallFromNextDocument(entryFile, dryRun, verbose);
|
||||||
|
} else if (entryFile.includes('_app')) {
|
||||||
|
successCount += uninstallFromNextApp(entryFile, dryRun, verbose);
|
||||||
|
}
|
||||||
|
} else if (projectType === 'next-app') {
|
||||||
|
successCount += uninstallFromNextAppLayout(entryFile, dryRun, verbose);
|
||||||
|
} else if (projectType === 'vite') {
|
||||||
|
successCount += uninstallFromViteConfig(entryFile, dryRun, verbose);
|
||||||
|
} else if (projectType === 'html-static' || projectType === 'create-react-app') {
|
||||||
|
successCount += uninstallFromHTML(entryFile, dryRun, verbose);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logError(`处理文件 ${entryFile} 失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
logSuccess(`成功清理 ${successCount} 个文件`);
|
||||||
|
} else {
|
||||||
|
logInfo('没有找到需要清理的注入脚本');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 HTML 文件卸载
|
||||||
|
function uninstallFromHTML(filePath, dryRun, verbose) {
|
||||||
|
const content = readFile(filePath);
|
||||||
|
const originalContent = content;
|
||||||
|
const newContent = removeInjectedScripts(content);
|
||||||
|
|
||||||
|
if (newContent === originalContent) {
|
||||||
|
log(`文件 ${filePath} 无需清理`, verbose);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
writeFile(filePath, newContent);
|
||||||
|
logSuccess(`已从 ${filePath} 移除注入的脚本`);
|
||||||
|
} else {
|
||||||
|
logInfo(`[DRY RUN] 将从 ${filePath} 移除注入的脚本`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Vite 配置卸载
|
||||||
|
function uninstallFromViteConfig(filePath, dryRun, verbose) {
|
||||||
|
const content = readFile(filePath);
|
||||||
|
const originalContent = content;
|
||||||
|
const newContent = removeViteInjection(content);
|
||||||
|
|
||||||
|
if (newContent === originalContent) {
|
||||||
|
log(`文件 ${filePath} 无需清理`, verbose);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
writeFile(filePath, newContent);
|
||||||
|
logSuccess(`已从 ${filePath} 移除注入的 Vite 插件`);
|
||||||
|
} else {
|
||||||
|
logInfo(`[DRY RUN] 将从 ${filePath} 移除注入的插件`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Next.js Document 卸载
|
||||||
|
function uninstallFromNextDocument(filePath, dryRun, verbose) {
|
||||||
|
const content = readFile(filePath);
|
||||||
|
const originalContent = content;
|
||||||
|
const newContent = removeNextDocumentInjection(content);
|
||||||
|
|
||||||
|
if (newContent === originalContent) {
|
||||||
|
log(`文件 ${filePath} 无需清理`, verbose);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
writeFile(filePath, newContent);
|
||||||
|
logSuccess(`已从 ${filePath} 移除注入的脚本`);
|
||||||
|
} else {
|
||||||
|
logInfo(`[DRY RUN] 将从 ${filePath} 移除注入的脚本`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Next.js App 卸载
|
||||||
|
function uninstallFromNextApp(filePath, dryRun, verbose) {
|
||||||
|
const content = readFile(filePath);
|
||||||
|
const originalContent = content;
|
||||||
|
const newContent = removeNextAppInjection(content);
|
||||||
|
|
||||||
|
if (newContent === originalContent) {
|
||||||
|
log(`文件 ${filePath} 无需清理`, verbose);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
writeFile(filePath, newContent);
|
||||||
|
logSuccess(`已从 ${filePath} 移除注入的脚本`);
|
||||||
|
} else {
|
||||||
|
logInfo(`[DRY RUN] 将从 ${filePath} 移除注入的脚本`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Next.js App Layout 卸载
|
||||||
|
function uninstallFromNextAppLayout(filePath, dryRun, verbose) {
|
||||||
|
const content = readFile(filePath);
|
||||||
|
const originalContent = content;
|
||||||
|
const newContent = removeNextAppLayoutInjection(content);
|
||||||
|
|
||||||
|
if (newContent === originalContent) {
|
||||||
|
log(`文件 ${filePath} 无需清理`, verbose);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
writeFile(filePath, newContent);
|
||||||
|
logSuccess(`已从 ${filePath} 移除注入的脚本`);
|
||||||
|
} else {
|
||||||
|
logInfo(`[DRY RUN] 将从 ${filePath} 移除注入的脚本`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能注入 - 根据项目类型选择最佳注入方式
|
||||||
|
export function smartInject(options = {}) {
|
||||||
|
const { remote, file, dryRun = false, verbose = false } = options;
|
||||||
|
|
||||||
|
// 检测项目类型
|
||||||
|
const projectType = detectProjectType();
|
||||||
|
// 只在 verbose 模式下输出检测到的项目类型(避免重复)
|
||||||
|
log(`检测到项目类型: ${projectType}`, verbose);
|
||||||
|
|
||||||
|
// 生成脚本标签
|
||||||
|
const remoteType = parseRemoteType(remote);
|
||||||
|
const scriptTag = generateScriptTag(remote, remoteType);
|
||||||
|
|
||||||
|
log(`脚本类型: ${remoteType}`, verbose);
|
||||||
|
log(`脚本标签: ${scriptTag}`, verbose);
|
||||||
|
|
||||||
|
// 查找入口文件
|
||||||
|
const entryFiles = file ? [file] : findFrameworkEntry(projectType);
|
||||||
|
|
||||||
|
if (entryFiles.length === 0) {
|
||||||
|
logError('未找到需要处理的文件');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`找到 ${entryFiles.length} 个文件:`, verbose);
|
||||||
|
entryFiles.forEach(f => log(` - ${f}`, verbose));
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const entryFile of entryFiles) {
|
||||||
|
try {
|
||||||
|
// 检查文件是否存在
|
||||||
|
if (!fs.existsSync(path.join(process.cwd(), entryFile))) {
|
||||||
|
log(`文件 ${entryFile} 不存在,跳过`, verbose);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 Next.js Pages Router
|
||||||
|
if (projectType === 'next-pages' || projectType === 'next-hybrid') {
|
||||||
|
if (entryFile.includes('_document') || entryFile.includes('_app')) {
|
||||||
|
if (injectToNextDocument(entryFile, scriptTag, remote)) {
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理 Next.js App Router
|
||||||
|
else if (projectType === 'next-app') {
|
||||||
|
if (injectToNextAppLayout(entryFile, scriptTag, remote)) {
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理 Vite 项目
|
||||||
|
else if (projectType === 'vite') {
|
||||||
|
if (injectToViteConfig(entryFile, remote, scriptTag)) {
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理 HTML 和 create-react-app
|
||||||
|
else if (projectType === 'html-static' || projectType === 'create-react-app') {
|
||||||
|
let htmlContent = readFile(entryFile);
|
||||||
|
const originalContent = htmlContent;
|
||||||
|
// 传入 remote URL 以使用动态时间戳防止缓存
|
||||||
|
const newContent = injectScriptToHtml(htmlContent, scriptTag, 'dev-inject', remote);
|
||||||
|
|
||||||
|
if (newContent !== originalContent) {
|
||||||
|
if (!dryRun) {
|
||||||
|
writeFile(entryFile, newContent);
|
||||||
|
logSuccess(`已注入脚本到 ${entryFile}`);
|
||||||
|
} else {
|
||||||
|
logInfo(`[DRY RUN] 将注入脚本到 ${entryFile}`);
|
||||||
|
}
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
log(`文件 ${entryFile} 已包含注入内容`, verbose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 收集错误,但不立即报错
|
||||||
|
errors.push(`处理文件 ${entryFile} 失败: ${error.message}`);
|
||||||
|
log(`处理文件 ${entryFile} 失败: ${error.message}`, verbose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有在所有尝试都失败后才报错
|
||||||
|
if (successCount > 0) {
|
||||||
|
logSuccess(`成功处理 ${successCount} 个文件`);
|
||||||
|
|
||||||
|
// 只在 verbose 模式下输出详细信息
|
||||||
|
log(`项目类型: ${projectType}`, verbose);
|
||||||
|
log(`脚本地址: ${remote}`, verbose);
|
||||||
|
|
||||||
|
// 统一输出重启提示(不重复)
|
||||||
|
if (projectType.startsWith('next-')) {
|
||||||
|
logInfo('💡 Next.js 项目: 脚本已注入到组件中,需要重启开发服务器');
|
||||||
|
} else if (projectType === 'vite') {
|
||||||
|
logInfo('💡 Vite 项目: 脚本已注入到配置文件,需要重启开发服务器');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有部分错误,显示警告
|
||||||
|
if (errors.length > 0) {
|
||||||
|
logInfo(`⚠️ 部分文件处理失败: ${errors.length} 个错误`);
|
||||||
|
errors.forEach(error => log(` - ${error}`, verbose));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 所有尝试都失败了
|
||||||
|
logError('没有成功处理任何文件');
|
||||||
|
if (errors.length > 0) {
|
||||||
|
logError('错误详情:');
|
||||||
|
errors.forEach(error => logError(` - ${error}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return successCount > 0;
|
||||||
|
}
|
||||||
115
qiming-dev-inject/lib/help.js
Normal file
115
qiming-dev-inject/lib/help.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
|
||||||
|
export function showHelp() {
|
||||||
|
console.log(`
|
||||||
|
@xagi/dev-inject - 开发环境脚本注入工具
|
||||||
|
|
||||||
|
用法:
|
||||||
|
npx @xagi/dev-inject <命令> [选项]
|
||||||
|
|
||||||
|
或使用 pnpm:
|
||||||
|
pnpm dlx @xagi/dev-inject <命令> [选项]
|
||||||
|
|
||||||
|
命令:
|
||||||
|
install 注入脚本到 HTML 文件
|
||||||
|
uninstall 从 HTML 文件中移除注入的脚本
|
||||||
|
|
||||||
|
选项:
|
||||||
|
--remote <url|path> 脚本地址(可选,默认值: /sdk/dev-monitor.js)
|
||||||
|
- 远程URL: http://localhost:9000/monitor.js
|
||||||
|
- 绝对路径: /scripts/monitor.js
|
||||||
|
|
||||||
|
--file <path> 指定要处理的 HTML 文件(可选)
|
||||||
|
- 如果不指定,会自动查找项目中的 HTML 文件
|
||||||
|
|
||||||
|
--dry-run 预览模式,显示将要执行的操作但不实际修改文件
|
||||||
|
|
||||||
|
--verbose 显示详细输出信息
|
||||||
|
|
||||||
|
--framework, -f 使用框架感知注入模式
|
||||||
|
- 自动检测 Vite、Next.js 等框架
|
||||||
|
- 使用最佳注入方式,无需手动修改 HTML
|
||||||
|
|
||||||
|
--help, -h 显示帮助信息
|
||||||
|
--version, -v 显示版本信息
|
||||||
|
|
||||||
|
示例:
|
||||||
|
# 静默安装(推荐,自动确认,无需交互)
|
||||||
|
npx -y @xagi/dev-inject install --framework
|
||||||
|
|
||||||
|
# 使用默认脚本地址 (/sdk/dev-monitor.js)
|
||||||
|
npx @xagi/dev-inject install
|
||||||
|
|
||||||
|
# 使用默认地址 + 框架感知注入
|
||||||
|
npx @xagi/dev-inject install --framework
|
||||||
|
|
||||||
|
# 传统 HTML 注入
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js
|
||||||
|
|
||||||
|
# 框架感知注入(推荐)
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||||
|
|
||||||
|
# Vite 项目自动注入
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js -f --verbose
|
||||||
|
|
||||||
|
# Next.js App Router 注入
|
||||||
|
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||||
|
|
||||||
|
# Next.js Pages Router 注入
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js -f
|
||||||
|
|
||||||
|
# 指定特定 HTML 文件(传统模式)
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/monitor.js --file=./public/index.html
|
||||||
|
|
||||||
|
# 使用 pnpm dlx
|
||||||
|
pnpm dlx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||||
|
|
||||||
|
# 预览将要执行的操作
|
||||||
|
npx @xagi/dev-inject install --remote=/scripts/monitor.js --framework --dry-run
|
||||||
|
|
||||||
|
# 移除框架注入
|
||||||
|
npx @xagi/dev-inject uninstall --framework
|
||||||
|
|
||||||
|
# 移除传统注入
|
||||||
|
npx @xagi/dev-inject uninstall
|
||||||
|
|
||||||
|
支持的脚本格式:
|
||||||
|
- 远程 HTTP/HTTPS URL
|
||||||
|
- 以 / 开头的绝对路径(相对于网站根目录)
|
||||||
|
|
||||||
|
框架支持:
|
||||||
|
🟢 Vite: 自动注入 Vite 插件到 vite.config.js
|
||||||
|
🟢 Next.js App Router: 注入到 app/layout.tsx
|
||||||
|
🟢 Next.js Pages Router: 注入到 pages/_document.tsx
|
||||||
|
🟢 Create React App: 注入到 public/index.html
|
||||||
|
🟢 传统 HTML: 直接修改 HTML 文件
|
||||||
|
|
||||||
|
框架注入优势:
|
||||||
|
✅ 自动检测项目类型
|
||||||
|
✅ 使用最佳注入方式
|
||||||
|
✅ 仅在开发环境生效
|
||||||
|
✅ 零侵入性修改
|
||||||
|
✅ 支持热重载
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- 使用 --framework 会自动选择最佳注入方式
|
||||||
|
- 框架注入后需要重启开发服务器
|
||||||
|
- 传统模式脚本会被注入到 HTML 文件的 </head> 标签之前
|
||||||
|
- 重复执行 install 会自动替换之前的注入
|
||||||
|
- 使用 uninstall 可以完全移除所有注入的脚本
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showVersion() {
|
||||||
|
try {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const packagePath = join(__dirname, '../package.json');
|
||||||
|
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
||||||
|
console.log(`@xagi/dev-inject v${packageJson.version}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('@xagi/dev-inject v1.0.0');
|
||||||
|
}
|
||||||
|
}
|
||||||
206
qiming-dev-inject/lib/inject.js
Normal file
206
qiming-dev-inject/lib/inject.js
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import {
|
||||||
|
lookupFiles,
|
||||||
|
readFile,
|
||||||
|
writeFile,
|
||||||
|
parseRemoteType,
|
||||||
|
generateScriptTag,
|
||||||
|
injectScriptToHtml,
|
||||||
|
removeInjectedScripts,
|
||||||
|
log,
|
||||||
|
logInfo,
|
||||||
|
logSuccess,
|
||||||
|
logError,
|
||||||
|
} from './utils.js';
|
||||||
|
import {
|
||||||
|
detectProjectType,
|
||||||
|
smartInject as smartInjectFramework,
|
||||||
|
} from './framework-inject.js';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前包版本号
|
||||||
|
*/
|
||||||
|
function getVersion() {
|
||||||
|
try {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const packagePath = join(__dirname, '..', 'package.json');
|
||||||
|
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
||||||
|
return packageJson.version;
|
||||||
|
} catch (error) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装脚本 - 支持框架感知
|
||||||
|
*/
|
||||||
|
export async function installScript(options = {}) {
|
||||||
|
const {
|
||||||
|
remote,
|
||||||
|
file,
|
||||||
|
dryRun = false,
|
||||||
|
verbose = false,
|
||||||
|
framework = false,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const version = getVersion();
|
||||||
|
logInfo(`开始执行脚本注入... (v${version})`);
|
||||||
|
|
||||||
|
// 检测是否使用框架注入模式
|
||||||
|
if (framework) {
|
||||||
|
logInfo('使用框架感知注入模式...');
|
||||||
|
|
||||||
|
// 检测项目类型(只在 verbose 模式下输出,避免重复)
|
||||||
|
const projectType = detectProjectType();
|
||||||
|
log(`检测到项目类型: ${projectType}`, verbose);
|
||||||
|
|
||||||
|
// 对于现代框架,使用智能注入
|
||||||
|
if (
|
||||||
|
['vite', 'next-app', 'next-pages', 'next-hybrid'].includes(projectType)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const success = smartInjectFramework({ remote, dryRun, verbose });
|
||||||
|
// smartInjectFramework 内部已经输出了所有必要的日志,这里不需要重复输出
|
||||||
|
return success;
|
||||||
|
} catch (error) {
|
||||||
|
logError(`框架注入失败: ${error.message}`);
|
||||||
|
logInfo('回退到传统 HTML 注入模式...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 传统 HTML 注入模式(兜底方案)
|
||||||
|
logInfo('使用传统 HTML 注入模式...');
|
||||||
|
|
||||||
|
// 查找 HTML 文件
|
||||||
|
const targetFiles = file ? [file] : lookupFiles();
|
||||||
|
|
||||||
|
if (targetFiles.length === 0) {
|
||||||
|
throw new Error('未找到 HTML 文件');
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`找到 ${targetFiles.length} 个 HTML 文件:`, verbose);
|
||||||
|
targetFiles.forEach(f => log(` - ${f}`, verbose));
|
||||||
|
|
||||||
|
// 解析远程脚本类型和生成脚本标签
|
||||||
|
const remoteType = parseRemoteType(remote);
|
||||||
|
const scriptTag = generateScriptTag(remote, remoteType);
|
||||||
|
|
||||||
|
log(`脚本类型: ${remoteType}`, verbose);
|
||||||
|
log(`脚本标签: ${scriptTag}`, verbose);
|
||||||
|
|
||||||
|
// 处理每个 HTML 文件
|
||||||
|
let successCount = 0;
|
||||||
|
|
||||||
|
for (const htmlFile of targetFiles) {
|
||||||
|
try {
|
||||||
|
// 读取 HTML 内容
|
||||||
|
let htmlContent = readFile(htmlFile);
|
||||||
|
const originalContent = htmlContent;
|
||||||
|
|
||||||
|
// 移除之前注入的脚本(避免重复)
|
||||||
|
htmlContent = removeInjectedScripts(htmlContent);
|
||||||
|
|
||||||
|
// 注入新脚本 - 传入 remote URL 以使用动态时间戳
|
||||||
|
const newContent = injectScriptToHtml(htmlContent, scriptTag, 'dev-inject', remote);
|
||||||
|
|
||||||
|
// 检查是否有变化
|
||||||
|
if (newContent === originalContent) {
|
||||||
|
log(`文件 ${htmlFile} 无需更新`, verbose);
|
||||||
|
successCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
if (!dryRun) {
|
||||||
|
writeFile(htmlFile, newContent);
|
||||||
|
logSuccess(`已注入脚本到 ${htmlFile}`);
|
||||||
|
} else {
|
||||||
|
logInfo(`[DRY RUN] 将注入脚本到 ${htmlFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
logError(`处理文件 ${htmlFile} 失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
logSuccess(`成功处理 ${successCount} 个文件`);
|
||||||
|
logInfo(`脚本地址: ${remote}`);
|
||||||
|
|
||||||
|
if (remoteType === 'absolute-path') {
|
||||||
|
logInfo('提示:请确保静态文件服务器可以访问该路径');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logError('没有成功处理任何文件');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载脚本从 HTML 文件
|
||||||
|
*/
|
||||||
|
export async function uninstallScript(options = {}) {
|
||||||
|
const { file, dryRun = false, verbose = false, framework } = options;
|
||||||
|
|
||||||
|
// 如果指定了 framework 标志,使用框架感知卸载
|
||||||
|
if (framework) {
|
||||||
|
const { smartUninstall } = await import('./framework-inject.js');
|
||||||
|
return smartUninstall(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo('开始移除注入的脚本...');
|
||||||
|
|
||||||
|
// 查找 HTML 文件
|
||||||
|
const targetFiles = file ? [file] : lookupFiles();
|
||||||
|
|
||||||
|
if (targetFiles.length === 0) {
|
||||||
|
logInfo('未找到 HTML 文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`找到 ${targetFiles.length} 个 HTML 文件:`, verbose);
|
||||||
|
targetFiles.forEach(f => log(` - ${f}`, verbose));
|
||||||
|
|
||||||
|
// 处理每个 HTML 文件
|
||||||
|
let successCount = 0;
|
||||||
|
|
||||||
|
for (const htmlFile of targetFiles) {
|
||||||
|
try {
|
||||||
|
// 读取 HTML 内容
|
||||||
|
let htmlContent = readFile(htmlFile);
|
||||||
|
const originalContent = htmlContent;
|
||||||
|
|
||||||
|
// 移除注入的脚本
|
||||||
|
const newContent = removeInjectedScripts(htmlContent);
|
||||||
|
|
||||||
|
// 检查是否有变化
|
||||||
|
if (newContent === originalContent) {
|
||||||
|
log(`文件 ${htmlFile} 无需清理`, verbose);
|
||||||
|
successCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
if (!dryRun) {
|
||||||
|
writeFile(htmlFile, newContent);
|
||||||
|
logSuccess(`已从 ${htmlFile} 移除注入的脚本`);
|
||||||
|
} else {
|
||||||
|
logInfo(`[DRY RUN] 将从 ${htmlFile} 移除注入的脚本`);
|
||||||
|
}
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
logError(`处理文件 ${htmlFile} 失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
logSuccess(`成功清理 ${successCount} 个文件`);
|
||||||
|
} else {
|
||||||
|
logInfo('没有找到需要清理的注入脚本');
|
||||||
|
}
|
||||||
|
}
|
||||||
275
qiming-dev-inject/lib/utils.js
Normal file
275
qiming-dev-inject/lib/utils.js
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查找项目中的 HTML 文件
|
||||||
|
*/
|
||||||
|
export function lookupFiles(startDir = process.cwd()) {
|
||||||
|
const htmlFiles = [];
|
||||||
|
|
||||||
|
function searchDirectory(dir) {
|
||||||
|
try {
|
||||||
|
const files = readdirSync(dir);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = join(dir, file);
|
||||||
|
const stat = statSync(fullPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
// 跳过 node_modules 等目录
|
||||||
|
if (!['node_modules', '.git', 'dist', 'build'].includes(file)) {
|
||||||
|
searchDirectory(fullPath);
|
||||||
|
}
|
||||||
|
} else if (file.endsWith('.html')) {
|
||||||
|
htmlFiles.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略无法访问的目录
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchDirectory(startDir);
|
||||||
|
|
||||||
|
// 优先返回 index.html,否则返回第一个找到的 HTML 文件
|
||||||
|
const indexHtml = htmlFiles.find(file => file.endsWith('index.html'));
|
||||||
|
return indexHtml ? [indexHtml] : htmlFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取文件内容
|
||||||
|
*/
|
||||||
|
export function readFile(filePath) {
|
||||||
|
try {
|
||||||
|
return readFileSync(filePath, 'utf8');
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`无法读取文件 ${filePath}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入文件内容
|
||||||
|
*/
|
||||||
|
export function writeFile(filePath, content) {
|
||||||
|
try {
|
||||||
|
writeFileSync(filePath, content, 'utf8');
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`无法写入文件 ${filePath}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析远程脚本类型
|
||||||
|
*/
|
||||||
|
export function parseRemoteType(remote) {
|
||||||
|
if (remote.startsWith('http://') || remote.startsWith('https://')) {
|
||||||
|
return 'url';
|
||||||
|
} else if (remote.startsWith('/')) {
|
||||||
|
return 'absolute-path';
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`不支持的远程路径格式: ${remote}。请使用完整 URL (http://...) 或绝对路径 (/path/to/script.js)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成脚本标签
|
||||||
|
*/
|
||||||
|
export function generateScriptTag(remote, type) {
|
||||||
|
// 所有类型都添加时间戳参数避免缓存
|
||||||
|
const separator = remote.includes('?') ? '&' : '?';
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
if (type === 'url') {
|
||||||
|
return `<script src="${remote}${separator}t=${timestamp}"></script>`;
|
||||||
|
} else if (type === 'absolute-path') {
|
||||||
|
return `<script src="${remote}${separator}t=${timestamp}"></script>`;
|
||||||
|
} else {
|
||||||
|
throw new Error(`未知的远程类型: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 HTML 内容中移除注入的脚本 - 同时清理前后的空白行
|
||||||
|
*/
|
||||||
|
export function removeInjectedScripts(htmlContent, identifier = 'dev-inject') {
|
||||||
|
// 使用标准的开始/结束标识移除注入的代码
|
||||||
|
// 匹配包括前后换行符和空白在内的完整块
|
||||||
|
// 使用非贪婪匹配确保只匹配一个注入块
|
||||||
|
const injectBlockRegex = /(\r?\n)?\s*<!-- DEV-INJECT-START -->[\s\S]*?<!-- DEV-INJECT-END -->\s*(\r?\n)?/g;
|
||||||
|
|
||||||
|
let cleaned = htmlContent.replace(injectBlockRegex, (match, beforeNewline, afterNewline) => {
|
||||||
|
// 如果匹配块前后都有换行符,只保留一个换行符
|
||||||
|
if (beforeNewline && afterNewline) {
|
||||||
|
return '\n';
|
||||||
|
}
|
||||||
|
// 如果只有前面的换行符,保留它
|
||||||
|
if (beforeNewline) {
|
||||||
|
return beforeNewline;
|
||||||
|
}
|
||||||
|
// 如果只有后面的换行符,保留它
|
||||||
|
if (afterNewline) {
|
||||||
|
return afterNewline;
|
||||||
|
}
|
||||||
|
// 如果都没有,返回空字符串
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清理移除后可能产生的多余空白行
|
||||||
|
// 清理 <head> 标签后的多余空白行(保留一个换行)
|
||||||
|
cleaned = cleaned.replace(/(<head[^>]*>)\s*\n\s*\n+(\s*)/g, '$1\n$2');
|
||||||
|
// 清理 </head> 前的多余空白行(保留一个换行)
|
||||||
|
cleaned = cleaned.replace(/(\s*)\n\s*\n+(\s*<\/head>)/g, '$1\n$2');
|
||||||
|
// 清理连续的多个空白行(保留最多一个空行)
|
||||||
|
cleaned = cleaned.replace(/\n\s*\n\s*\n+/g, '\n\n');
|
||||||
|
// 清理空行(只包含空白字符的行)
|
||||||
|
cleaned = cleaned.replace(/^\s+$/gm, '');
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向 HTML 中注入脚本 - 使用动态时间戳防止缓存
|
||||||
|
*/
|
||||||
|
export function injectScriptToHtml(
|
||||||
|
htmlContent,
|
||||||
|
scriptTag,
|
||||||
|
identifier = 'dev-inject',
|
||||||
|
remote = null
|
||||||
|
) {
|
||||||
|
// 先清除之前的注入
|
||||||
|
htmlContent = removeInjectedScripts(htmlContent, identifier);
|
||||||
|
|
||||||
|
// 生成脚本内容 - 优先使用动态时间戳方式
|
||||||
|
// 注意:脚本内容前后不包含额外的换行符,由插入逻辑统一处理
|
||||||
|
let scriptContent;
|
||||||
|
if (remote) {
|
||||||
|
// 使用动态脚本加载器(推荐方式,防止缓存)
|
||||||
|
const safeRemote = JSON.stringify(remote);
|
||||||
|
const scriptId = `${identifier}-monitor`;
|
||||||
|
scriptContent = `<!-- DEV-INJECT-START -->
|
||||||
|
<script data-id="${scriptId}">
|
||||||
|
(function() {
|
||||||
|
const remote = ${safeRemote};
|
||||||
|
const separator = remote.includes('?') ? '&' : '?';
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = remote + separator + 't=' + Date.now();
|
||||||
|
script.dataset.id = '${scriptId}-script';
|
||||||
|
script.defer = true;
|
||||||
|
// 防止重复注入
|
||||||
|
if (!document.querySelector('[data-id="${scriptId}-script"]')) {
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<!-- DEV-INJECT-END -->`;
|
||||||
|
} else {
|
||||||
|
// 向后兼容:从 scriptTag 中提取 URL 并添加时间戳
|
||||||
|
const scriptSrc = scriptTag.match(/src="([^"]*)"/)?.[1] || '';
|
||||||
|
if (scriptSrc) {
|
||||||
|
// 提取基础 URL(移除可能已存在的时间戳参数)
|
||||||
|
const baseUrl = scriptSrc.split('?')[0];
|
||||||
|
const separator = scriptSrc.includes('?') ? '&' : '?';
|
||||||
|
scriptContent = `<!-- DEV-INJECT-START -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const remote = ${JSON.stringify(baseUrl)};
|
||||||
|
const separator = remote.includes('?') ? '&' : '?';
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = remote + separator + 't=' + Date.now();
|
||||||
|
script.defer = true;
|
||||||
|
// 防止重复注入
|
||||||
|
if (!document.querySelector('script[src*="' + remote.split('/').pop() + '"]')) {
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<!-- DEV-INJECT-END -->`;
|
||||||
|
} else {
|
||||||
|
// 如果无法提取 URL,使用旧的静态方式
|
||||||
|
scriptContent = `<!-- DEV-INJECT-START -->
|
||||||
|
${scriptTag}
|
||||||
|
<!-- DEV-INJECT-END -->`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找 </head> 标签
|
||||||
|
const headEndIndex = htmlContent.lastIndexOf('</head>');
|
||||||
|
|
||||||
|
if (headEndIndex !== -1) {
|
||||||
|
// 在 </head> 之前注入
|
||||||
|
const beforeHead = htmlContent.substring(0, headEndIndex);
|
||||||
|
const afterHead = htmlContent.substring(headEndIndex);
|
||||||
|
|
||||||
|
// 检查 </head> 前是否有非空白内容
|
||||||
|
const beforeHeadTrimmed = beforeHead.trimEnd();
|
||||||
|
const needsNewlineBefore = beforeHeadTrimmed.length > 0 && !beforeHeadTrimmed.endsWith('\n');
|
||||||
|
|
||||||
|
// 构建注入内容,统一格式
|
||||||
|
let result;
|
||||||
|
if (needsNewlineBefore) {
|
||||||
|
// 如果前面有内容,添加换行符后插入脚本
|
||||||
|
result = `${beforeHeadTrimmed}\n${scriptContent}\n${afterHead}`;
|
||||||
|
} else {
|
||||||
|
// 如果前面是空白或换行,直接插入(脚本内容已有标识注释)
|
||||||
|
// 确保脚本前后有适当的换行
|
||||||
|
const trimmedBefore = beforeHead.trimEnd();
|
||||||
|
if (trimmedBefore.length === 0) {
|
||||||
|
// 如果前面完全是空白,直接插入
|
||||||
|
result = `${beforeHead}${scriptContent}\n${afterHead}`;
|
||||||
|
} else {
|
||||||
|
// 前面有内容但末尾是换行,直接插入
|
||||||
|
result = `${beforeHead}${scriptContent}\n${afterHead}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理可能的多余空白行(保留最多一个空行)
|
||||||
|
// 清理 <head> 标签后的多余空白行
|
||||||
|
result = result.replace(/(<head[^>]*>)\s*\n\s*\n+(\s*)/g, '$1\n$2');
|
||||||
|
// 清理 </head> 前的多余空白行
|
||||||
|
result = result.replace(/(\s*)\n\s*\n+(\s*<\/head>)/g, '$1\n$2');
|
||||||
|
// 清理连续的多个空白行
|
||||||
|
result = result.replace(/\n\s*\n\s*\n+/g, '\n\n');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
// 如果没有找到 </head>,查找 <body> 标签
|
||||||
|
const bodyStartIndex = htmlContent.indexOf('<body');
|
||||||
|
|
||||||
|
if (bodyStartIndex !== -1) {
|
||||||
|
const bodyEndIndex = htmlContent.indexOf('>', bodyStartIndex) + 1;
|
||||||
|
const beforeBody = htmlContent.substring(0, bodyEndIndex);
|
||||||
|
const afterBody = htmlContent.substring(bodyEndIndex);
|
||||||
|
return `${beforeBody}
|
||||||
|
${scriptContent}
|
||||||
|
${afterBody}`;
|
||||||
|
} else {
|
||||||
|
// 如果都没有找到,在文件末尾添加
|
||||||
|
return `${htmlContent}
|
||||||
|
${scriptContent}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志输出函数
|
||||||
|
*/
|
||||||
|
export function log(message, verbose = false) {
|
||||||
|
if (verbose) {
|
||||||
|
console.log(`[dev-inject] ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logError(message) {
|
||||||
|
console.error(`❌ [dev-inject] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logSuccess(message) {
|
||||||
|
console.log(`✅ [dev-inject] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logInfo(message) {
|
||||||
|
console.log(`ℹ️ [dev-inject] ${message}`);
|
||||||
|
}
|
||||||
50
qiming-dev-inject/package.json
Normal file
50
qiming-dev-inject/package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "@xagi/dev-inject",
|
||||||
|
"version": "1.0.4",
|
||||||
|
"description": "框架感知的开发环境脚本注入工具 - 支持 Vite、Next.js、React 等现代框架的智能脚本注入",
|
||||||
|
"type": "module",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"bin": {
|
||||||
|
"dev-inject": "bin/index.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin/",
|
||||||
|
"lib/",
|
||||||
|
"scripts/",
|
||||||
|
"*.md",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "echo 'No build needed for Node.js CLI'",
|
||||||
|
"prepare": "chmod +x bin/index.js",
|
||||||
|
"test": "node scripts/test-projects.js all",
|
||||||
|
"test:all": "node scripts/test-projects.js all",
|
||||||
|
"test:install": "node scripts/test-projects.js install",
|
||||||
|
"test:uninstall": "node scripts/test-projects.js uninstall",
|
||||||
|
"test:html": "cd test-projects/html-project && node ../../bin/index.js install --remote=/sdk/dev-monitor.js --framework && node ../../bin/index.js uninstall --framework",
|
||||||
|
"test:vite": "cd test-projects/vite-project && node ../../bin/index.js install --remote=/sdk/dev-monitor.js --framework && node ../../bin/index.js uninstall --framework",
|
||||||
|
"test:next-pages": "cd test-projects/next-pages-project && node ../../bin/index.js install --remote=/sdk/dev-monitor.js --framework && node ../../bin/index.js uninstall --framework",
|
||||||
|
"test:next-app": "cd test-projects/next-app-project && node ../../bin/index.js install --remote=/sdk/dev-monitor.js --framework && node ../../bin/index.js uninstall --framework"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"development",
|
||||||
|
"script-injection",
|
||||||
|
"html",
|
||||||
|
"cli",
|
||||||
|
"remote-script",
|
||||||
|
"local-script"
|
||||||
|
],
|
||||||
|
"author": "dongdada29",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/dongdada29/dev-inject.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/dongdada29/dev-inject/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/dongdada29/dev-inject#readme"
|
||||||
|
}
|
||||||
223
qiming-dev-inject/scripts/dev-monitor.js
Normal file
223
qiming-dev-inject/scripts/dev-monitor.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* 开发环境监控脚本
|
||||||
|
* 提供错误监控、性能监控、通信功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
const config = {
|
||||||
|
version: '1.0.0',
|
||||||
|
enabled: true,
|
||||||
|
logLevel: 'error', // 只记录错误级别日志
|
||||||
|
maxErrors: 10, // 减少存储量
|
||||||
|
maxLogs: 20, // 减少存储量
|
||||||
|
};
|
||||||
|
|
||||||
|
// 简化的监控数据存储
|
||||||
|
const monitorData = {
|
||||||
|
errors: [],
|
||||||
|
basicInfo: {
|
||||||
|
url: window.location.href,
|
||||||
|
userAgent: navigator.userAgent.split(' ')[0], // 只保留浏览器名称
|
||||||
|
},
|
||||||
|
ready: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 简化的日志函数 - 只记录错误
|
||||||
|
const logger = {
|
||||||
|
error: (message, details = null) => {
|
||||||
|
console.error('[Dev-Monitor ERROR]', message, details || '');
|
||||||
|
|
||||||
|
const errorData = {
|
||||||
|
message: typeof message === 'string' ? message : message.toString(),
|
||||||
|
details: details ? JSON.stringify(details).substring(0, 200) : null, // 限制详细信息长度
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
monitorData.errors.push(errorData);
|
||||||
|
|
||||||
|
// 限制错误数量
|
||||||
|
if (monitorData.errors.length > config.maxErrors) {
|
||||||
|
monitorData.errors.shift();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 简化的错误监控
|
||||||
|
function setupErrorMonitoring() {
|
||||||
|
// 全局错误捕获
|
||||||
|
window.addEventListener('error', function (event) {
|
||||||
|
const errorMsg = `${event.message} at ${event.filename}:${event.lineno}:${event.colno}`;
|
||||||
|
logger.error(errorMsg);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Promise 错误捕获
|
||||||
|
window.addEventListener('unhandledrejection', function (event) {
|
||||||
|
const errorMsg = `Promise rejection: ${event.reason}`;
|
||||||
|
logger.error(errorMsg);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 资源加载错误 - 只记录关键信息
|
||||||
|
window.addEventListener(
|
||||||
|
'error',
|
||||||
|
function (event) {
|
||||||
|
if (event.target !== window) {
|
||||||
|
const source = event.target.src || event.target.href || 'unknown';
|
||||||
|
logger.error(`Resource failed: ${source}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除复杂的性能监控和控制台拦截,专注于核心错误监控
|
||||||
|
|
||||||
|
// 简化的通信功能
|
||||||
|
function setupCommunication() {
|
||||||
|
// 向父窗口发送监控数据 - 只在有新错误时发送
|
||||||
|
function sendToParent() {
|
||||||
|
if (
|
||||||
|
window.parent &&
|
||||||
|
window.parent !== window &&
|
||||||
|
monitorData.errors.length > 0
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const summary = {
|
||||||
|
type: 'dev-monitor-summary',
|
||||||
|
errorCount: monitorData.errors.length,
|
||||||
|
latestError: monitorData.errors[monitorData.errors.length - 1],
|
||||||
|
url: monitorData.basicInfo.url,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
window.parent.postMessage(summary, '*');
|
||||||
|
} catch (e) {
|
||||||
|
// 静默处理错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听来自父窗口的请求
|
||||||
|
window.addEventListener('message', function (event) {
|
||||||
|
if (event.data && event.data.type === 'dev-monitor-request') {
|
||||||
|
const summary = {
|
||||||
|
type: 'dev-monitor-data',
|
||||||
|
errorCount: monitorData.errors.length,
|
||||||
|
errors: monitorData.errors.slice(-3), // 只发送最近3个错误
|
||||||
|
url: monitorData.basicInfo.url,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.parent.postMessage(summary, '*');
|
||||||
|
} catch (e) {
|
||||||
|
// 静默处理错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 降低发送频率 - 每10秒检查一次
|
||||||
|
setInterval(sendToParent, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简化的全局 API
|
||||||
|
window.DevMonitor = {
|
||||||
|
// 获取错误统计
|
||||||
|
getStats: function () {
|
||||||
|
return {
|
||||||
|
errorCount: monitorData.errors.length,
|
||||||
|
url: monitorData.basicInfo.url,
|
||||||
|
userAgent: monitorData.basicInfo.userAgent,
|
||||||
|
latestError: monitorData.errors[monitorData.errors.length - 1] || null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取所有错误(限制数量)
|
||||||
|
getErrors: function () {
|
||||||
|
return monitorData.errors.slice(-5); // 只返回最近5个错误
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除错误
|
||||||
|
clearErrors: function () {
|
||||||
|
monitorData.errors = [];
|
||||||
|
console.log('✅ DevMonitor errors cleared');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 显示简化监控面板
|
||||||
|
showPanel: function () {
|
||||||
|
// 移除现有面板
|
||||||
|
const existingPanel = document.getElementById('dev-monitor-panel');
|
||||||
|
if (existingPanel) {
|
||||||
|
existingPanel.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = document.createElement('div');
|
||||||
|
panel.id = 'dev-monitor-panel';
|
||||||
|
panel.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
width: 280px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
z-index: 999999;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const errors = monitorData.errors.slice(-3); // 只显示最近3个错误
|
||||||
|
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div style="border-bottom: 1px solid #333; margin-bottom: 10px; padding-bottom: 8px; display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<strong style="color: #4CAF50;">DevMonitor</strong>
|
||||||
|
<button onclick="this.parentElement.parentElement.remove()" style="background: #ff4444; border: none; color: white; padding: 2px 6px; border-radius: 3px; cursor: pointer;">×</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<strong>Errors (${monitorData.errors.length}):</strong>
|
||||||
|
</div>
|
||||||
|
${
|
||||||
|
errors.length
|
||||||
|
? errors
|
||||||
|
.map(
|
||||||
|
e => `
|
||||||
|
<div style="color: #ff6b6b; margin: 4px 0; padding: 4px; background: rgba(255,107,107,0.1); border-radius: 3px; font-size: 11px;">
|
||||||
|
${e.message}
|
||||||
|
${e.details ? `<br><small style="color: #999;">${e.details}</small>` : ''}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join('')
|
||||||
|
: '<div style="color: #666; font-style: italic;">No errors</div>'
|
||||||
|
}
|
||||||
|
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #333;">
|
||||||
|
<button onclick="DevMonitor.clearErrors(); DevMonitor.showPanel();" style="background: #333; border: none; color: white; padding: 6px 12px; border-radius: 3px; cursor: pointer; margin-right: 8px;">Clear</button>
|
||||||
|
<button onclick="console.log('Stats:', DevMonitor.getStats())" style="background: #2196F3; border: none; color: white; padding: 6px 12px; border-radius: 3px; cursor: pointer;">Stats</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 简化的初始化
|
||||||
|
function init() {
|
||||||
|
setupErrorMonitoring();
|
||||||
|
setupCommunication();
|
||||||
|
monitorData.ready = true;
|
||||||
|
|
||||||
|
// 简化的控制台提示
|
||||||
|
console.log(
|
||||||
|
'%cDevMonitor v' + config.version,
|
||||||
|
'color: #4CAF50; font-weight: bold;',
|
||||||
|
'- DevMonitor.showPanel()'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即初始化
|
||||||
|
init();
|
||||||
|
})();
|
||||||
534
qiming-dev-inject/scripts/test-projects.js
Normal file
534
qiming-dev-inject/scripts/test-projects.js
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试脚本 - 运行所有 test-projects 的注入和卸载测试
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { existsSync, readFileSync } from 'fs';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const rootDir = join(__dirname, '..');
|
||||||
|
const testProjectsDir = join(rootDir, 'test-projects');
|
||||||
|
|
||||||
|
// 测试项目列表
|
||||||
|
const testProjects = [
|
||||||
|
{ name: 'html-project', type: 'html-static' },
|
||||||
|
{ name: 'vite-project', type: 'vite' },
|
||||||
|
{ name: 'next-pages-project', type: 'next-pages' },
|
||||||
|
{ name: 'next-app-project', type: 'next-app' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 测试配置
|
||||||
|
const testConfig = {
|
||||||
|
remote: '/sdk/dev-monitor.js',
|
||||||
|
remoteUrl: 'https://testagent.xspaceagi.com/sdk/dev-monitor.js',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 颜色输出
|
||||||
|
const colors = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
bright: '\x1b[1m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
cyan: '\x1b[36m',
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(message, color = 'reset') {
|
||||||
|
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logSection(title) {
|
||||||
|
console.log('\n' + '='.repeat(60));
|
||||||
|
log(title, 'bright');
|
||||||
|
console.log('='.repeat(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
function logTest(name) {
|
||||||
|
log(`\n📦 测试项目: ${name}`, 'cyan');
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCommand(command, cwd) {
|
||||||
|
try {
|
||||||
|
const output = execSync(command, {
|
||||||
|
cwd,
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
return { success: true, output };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: error.stdout || error.stderr || error.message,
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试单个项目的安装(注入)
|
||||||
|
*/
|
||||||
|
function testInstall(project, remote, testName) {
|
||||||
|
const projectPath = join(testProjectsDir, project.name);
|
||||||
|
|
||||||
|
if (!existsSync(projectPath)) {
|
||||||
|
log(` ❌ 项目目录不存在: ${projectPath}`, 'red');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logTest(`${project.name} (${testName})`);
|
||||||
|
|
||||||
|
// 1. 先清理之前的注入(可选)
|
||||||
|
log(' 🔄 步骤 1: 清理之前的注入...', 'yellow');
|
||||||
|
const uninstallResult = runCommand(
|
||||||
|
`node ${join(rootDir, 'bin/index.js')} uninstall --framework`,
|
||||||
|
projectPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!uninstallResult.success && !uninstallResult.output.includes('未找到需要清理的注入脚本')) {
|
||||||
|
log(` ⚠️ 清理警告: ${uninstallResult.output}`, 'yellow');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 执行注入
|
||||||
|
log(' ➕ 步骤 2: 注入脚本...', 'yellow');
|
||||||
|
const installResult = runCommand(
|
||||||
|
`node ${join(rootDir, 'bin/index.js')} install --remote=${remote} --framework`,
|
||||||
|
projectPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!installResult.success) {
|
||||||
|
log(` ❌ 注入失败: ${installResult.error}`, 'red');
|
||||||
|
console.log(installResult.output);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(' ✅ 注入成功', 'green');
|
||||||
|
|
||||||
|
// 3. 验证注入结果
|
||||||
|
log(' 🔍 步骤 3: 验证注入结果...', 'yellow');
|
||||||
|
const verifyResult = verifyInjection(project, projectPath);
|
||||||
|
|
||||||
|
if (!verifyResult.success) {
|
||||||
|
log(` ❌ 验证失败: ${verifyResult.message}`, 'red');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(' ✅ 验证通过', 'green');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试单个项目的卸载
|
||||||
|
*/
|
||||||
|
function testUninstall(project) {
|
||||||
|
const projectPath = join(testProjectsDir, project.name);
|
||||||
|
|
||||||
|
if (!existsSync(projectPath)) {
|
||||||
|
log(` ❌ 项目目录不存在: ${projectPath}`, 'red');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logTest(`${project.name}`);
|
||||||
|
|
||||||
|
// 1. 先确保有注入内容(如果没有,先注入)
|
||||||
|
log(' 🔍 步骤 1: 检查是否有注入内容...', 'yellow');
|
||||||
|
const hasInjection = checkInjection(project, projectPath);
|
||||||
|
|
||||||
|
if (!hasInjection) {
|
||||||
|
log(' ⚠️ 未找到注入内容,先执行注入...', 'yellow');
|
||||||
|
const installResult = runCommand(
|
||||||
|
`node ${join(rootDir, 'bin/index.js')} install --remote=/sdk/dev-monitor.js --framework`,
|
||||||
|
projectPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!installResult.success) {
|
||||||
|
log(` ❌ 预注入失败: ${installResult.error}`, 'red');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log(' ✅ 预注入成功', 'green');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 执行卸载
|
||||||
|
log(' 🗑️ 步骤 2: 卸载注入...', 'yellow');
|
||||||
|
const uninstallResult = runCommand(
|
||||||
|
`node ${join(rootDir, 'bin/index.js')} uninstall --framework`,
|
||||||
|
projectPath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!uninstallResult.success) {
|
||||||
|
log(` ❌ 卸载失败: ${uninstallResult.error}`, 'red');
|
||||||
|
console.log(uninstallResult.output);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(' ✅ 卸载成功', 'green');
|
||||||
|
|
||||||
|
// 3. 验证卸载结果
|
||||||
|
log(' 🔍 步骤 3: 验证卸载结果...', 'yellow');
|
||||||
|
const verifyResult = verifyUninstall(project, projectPath);
|
||||||
|
|
||||||
|
if (!verifyResult.success) {
|
||||||
|
log(` ❌ 验证失败: ${verifyResult.message}`, 'red');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(' ✅ 验证通过', 'green');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有注入内容
|
||||||
|
*/
|
||||||
|
function checkInjection(project, projectPath) {
|
||||||
|
try {
|
||||||
|
switch (project.type) {
|
||||||
|
case 'html-static': {
|
||||||
|
const htmlFile = join(projectPath, 'index.html');
|
||||||
|
if (existsSync(htmlFile)) {
|
||||||
|
const content = readFileSync(htmlFile, 'utf8');
|
||||||
|
return content.includes('DEV-INJECT-START');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case 'vite': {
|
||||||
|
const configFile = join(projectPath, 'vite.config.js');
|
||||||
|
if (existsSync(configFile)) {
|
||||||
|
const content = readFileSync(configFile, 'utf8');
|
||||||
|
return content.includes('DEV-INJECT-START');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case 'next-pages': {
|
||||||
|
const docFile = join(projectPath, 'pages/_document.tsx');
|
||||||
|
if (existsSync(docFile)) {
|
||||||
|
const content = readFileSync(docFile, 'utf8');
|
||||||
|
return content.includes('DEV-INJECT-START');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case 'next-app': {
|
||||||
|
const layoutFile = join(projectPath, 'app/layout.tsx');
|
||||||
|
if (existsSync(layoutFile)) {
|
||||||
|
const content = readFileSync(layoutFile, 'utf8');
|
||||||
|
return content.includes('DEV-INJECT-START');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证注入结果
|
||||||
|
*/
|
||||||
|
function verifyInjection(project, projectPath) {
|
||||||
|
try {
|
||||||
|
switch (project.type) {
|
||||||
|
case 'html-static': {
|
||||||
|
const htmlFile = join(projectPath, 'index.html');
|
||||||
|
const content = readFileSync(htmlFile, 'utf8');
|
||||||
|
if (!content.includes('DEV-INJECT-START')) {
|
||||||
|
return { success: false, message: 'HTML 文件中未找到注入标记' };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'vite': {
|
||||||
|
const configFile = join(projectPath, 'vite.config.js');
|
||||||
|
const content = readFileSync(configFile, 'utf8');
|
||||||
|
if (!content.includes('DEV-INJECT-START')) {
|
||||||
|
return { success: false, message: 'Vite 配置文件中未找到注入标记' };
|
||||||
|
}
|
||||||
|
// 验证逗号位置(允许多种格式)
|
||||||
|
if (!content.includes('// <!-- DEV-INJECT-END -->')) {
|
||||||
|
return { success: false, message: '未找到结束标记' };
|
||||||
|
}
|
||||||
|
// 验证 index.html 未被修改
|
||||||
|
const htmlFile = join(projectPath, 'index.html');
|
||||||
|
const htmlContent = readFileSync(htmlFile, 'utf8');
|
||||||
|
if (htmlContent.includes('DEV-INJECT-START')) {
|
||||||
|
return { success: false, message: 'index.html 不应包含注入标记' };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'next-pages': {
|
||||||
|
const docFile = join(projectPath, 'pages/_document.tsx');
|
||||||
|
if (existsSync(docFile)) {
|
||||||
|
const content = readFileSync(docFile, 'utf8');
|
||||||
|
if (!content.includes('DEV-INJECT-START')) {
|
||||||
|
return { success: false, message: '_document.tsx 中未找到注入标记' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'next-app': {
|
||||||
|
const layoutFile = join(projectPath, 'app/layout.tsx');
|
||||||
|
if (existsSync(layoutFile)) {
|
||||||
|
const content = readFileSync(layoutFile, 'utf8');
|
||||||
|
if (!content.includes('DEV-INJECT-START')) {
|
||||||
|
return { success: false, message: 'layout.tsx 中未找到注入标记' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证卸载结果
|
||||||
|
*/
|
||||||
|
function verifyUninstall(project, projectPath) {
|
||||||
|
try {
|
||||||
|
switch (project.type) {
|
||||||
|
case 'html-static': {
|
||||||
|
const htmlFile = join(projectPath, 'index.html');
|
||||||
|
const content = readFileSync(htmlFile, 'utf8');
|
||||||
|
if (content.includes('DEV-INJECT-START')) {
|
||||||
|
return { success: false, message: 'HTML 文件中仍存在注入标记' };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'vite': {
|
||||||
|
const configFile = join(projectPath, 'vite.config.js');
|
||||||
|
const content = readFileSync(configFile, 'utf8');
|
||||||
|
if (content.includes('DEV-INJECT-START')) {
|
||||||
|
return { success: false, message: 'Vite 配置文件中仍存在注入标记' };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'next-pages': {
|
||||||
|
const docFile = join(projectPath, 'pages/_document.tsx');
|
||||||
|
if (existsSync(docFile)) {
|
||||||
|
const content = readFileSync(docFile, 'utf8');
|
||||||
|
if (content.includes('DEV-INJECT-START')) {
|
||||||
|
return { success: false, message: '_document.tsx 中仍存在注入标记' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'next-app': {
|
||||||
|
const layoutFile = join(projectPath, 'app/layout.tsx');
|
||||||
|
if (existsSync(layoutFile)) {
|
||||||
|
const content = readFileSync(layoutFile, 'utf8');
|
||||||
|
if (content.includes('DEV-INJECT-START')) {
|
||||||
|
return { success: false, message: 'layout.tsx 中仍存在注入标记' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行安装测试
|
||||||
|
*/
|
||||||
|
function runInstallTests() {
|
||||||
|
logSection('🚀 开始运行安装(注入)测试');
|
||||||
|
log(`测试目录: ${testProjectsDir}`, 'blue');
|
||||||
|
log(`测试项目数量: ${testProjects.length}`, 'blue');
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
total: 0,
|
||||||
|
passed: 0,
|
||||||
|
failed: 0,
|
||||||
|
projects: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试本地路径注入
|
||||||
|
logSection('📝 测试 1: 本地路径注入 (/sdk/dev-monitor.js)');
|
||||||
|
|
||||||
|
for (const project of testProjects) {
|
||||||
|
results.total++;
|
||||||
|
const success = testInstall(project, testConfig.remote, '本地路径');
|
||||||
|
results.projects.push({ project: project.name, test: '本地路径', success });
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
results.passed++;
|
||||||
|
} else {
|
||||||
|
results.failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试远程 URL 注入
|
||||||
|
logSection('🌐 测试 2: 远程 URL 注入 (https://testagent.xspaceagi.com/sdk/dev-monitor.js)');
|
||||||
|
|
||||||
|
for (const project of testProjects) {
|
||||||
|
results.total++;
|
||||||
|
const success = testInstall(project, testConfig.remoteUrl, '远程URL');
|
||||||
|
results.projects.push({ project: project.name, test: '远程URL', success });
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
results.passed++;
|
||||||
|
} else {
|
||||||
|
results.failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输出测试结果
|
||||||
|
logSection('📊 安装测试结果汇总');
|
||||||
|
log(`总测试数: ${results.total}`, 'blue');
|
||||||
|
log(`通过: ${results.passed}`, 'green');
|
||||||
|
log(`失败: ${results.failed}`, results.failed > 0 ? 'red' : 'green');
|
||||||
|
|
||||||
|
console.log('\n详细结果:');
|
||||||
|
results.projects.forEach(({ project, test, success }) => {
|
||||||
|
const icon = success ? '✅' : '❌';
|
||||||
|
const color = success ? 'green' : 'red';
|
||||||
|
log(` ${icon} ${project} - ${test}`, color);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回退出码
|
||||||
|
process.exit(results.failed > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行卸载测试
|
||||||
|
*/
|
||||||
|
function runUninstallTests() {
|
||||||
|
logSection('🚀 开始运行卸载测试');
|
||||||
|
log(`测试目录: ${testProjectsDir}`, 'blue');
|
||||||
|
log(`测试项目数量: ${testProjects.length}`, 'blue');
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
total: 0,
|
||||||
|
passed: 0,
|
||||||
|
failed: 0,
|
||||||
|
projects: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const project of testProjects) {
|
||||||
|
results.total++;
|
||||||
|
const success = testUninstall(project);
|
||||||
|
results.projects.push({ project: project.name, success });
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
results.passed++;
|
||||||
|
} else {
|
||||||
|
results.failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输出测试结果
|
||||||
|
logSection('📊 卸载测试结果汇总');
|
||||||
|
log(`总测试数: ${results.total}`, 'blue');
|
||||||
|
log(`通过: ${results.passed}`, 'green');
|
||||||
|
log(`失败: ${results.failed}`, results.failed > 0 ? 'red' : 'green');
|
||||||
|
|
||||||
|
console.log('\n详细结果:');
|
||||||
|
results.projects.forEach(({ project, success }) => {
|
||||||
|
const icon = success ? '✅' : '❌';
|
||||||
|
const color = success ? 'green' : 'red';
|
||||||
|
log(` ${icon} ${project}`, color);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回退出码
|
||||||
|
process.exit(results.failed > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行所有测试(安装 + 卸载)
|
||||||
|
*/
|
||||||
|
function runAllTests() {
|
||||||
|
logSection('🚀 开始运行完整测试(安装 + 卸载)');
|
||||||
|
log(`测试目录: ${testProjectsDir}`, 'blue');
|
||||||
|
log(`测试项目数量: ${testProjects.length}`, 'blue');
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
total: 0,
|
||||||
|
passed: 0,
|
||||||
|
failed: 0,
|
||||||
|
projects: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 先运行安装测试
|
||||||
|
logSection('📝 第一部分: 安装测试');
|
||||||
|
|
||||||
|
for (const project of testProjects) {
|
||||||
|
results.total++;
|
||||||
|
const success = testInstall(project, testConfig.remote, '本地路径');
|
||||||
|
results.projects.push({ project: project.name, test: '安装', success });
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
results.passed++;
|
||||||
|
} else {
|
||||||
|
results.failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再运行卸载测试
|
||||||
|
logSection('🗑️ 第二部分: 卸载测试');
|
||||||
|
|
||||||
|
for (const project of testProjects) {
|
||||||
|
results.total++;
|
||||||
|
const success = testUninstall(project);
|
||||||
|
results.projects.push({ project: project.name, test: '卸载', success });
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
results.passed++;
|
||||||
|
} else {
|
||||||
|
results.failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输出测试结果
|
||||||
|
logSection('📊 完整测试结果汇总');
|
||||||
|
log(`总测试数: ${results.total}`, 'blue');
|
||||||
|
log(`通过: ${results.passed}`, 'green');
|
||||||
|
log(`失败: ${results.failed}`, results.failed > 0 ? 'red' : 'green');
|
||||||
|
|
||||||
|
console.log('\n详细结果:');
|
||||||
|
results.projects.forEach(({ project, test, success }) => {
|
||||||
|
const icon = success ? '✅' : '❌';
|
||||||
|
const color = success ? 'green' : 'red';
|
||||||
|
log(` ${icon} ${project} - ${test}`, color);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回退出码
|
||||||
|
process.exit(results.failed > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析命令行参数
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const command = args[0] || 'all';
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
try {
|
||||||
|
switch (command) {
|
||||||
|
case 'install':
|
||||||
|
case 'i':
|
||||||
|
runInstallTests();
|
||||||
|
break;
|
||||||
|
case 'uninstall':
|
||||||
|
case 'u':
|
||||||
|
runUninstallTests();
|
||||||
|
break;
|
||||||
|
case 'all':
|
||||||
|
default:
|
||||||
|
runAllTests();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`\n❌ 测试执行失败: ${error.message}`, 'red');
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
124
qiming-dev-inject/test-projects/README.md
Normal file
124
qiming-dev-inject/test-projects/README.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# 测试项目集合
|
||||||
|
|
||||||
|
这个目录包含了用于测试 dev-inject 在不同框架和项目类型中的注入功能的示例项目。
|
||||||
|
|
||||||
|
## 项目列表
|
||||||
|
|
||||||
|
### 1. HTML 静态项目 (html-project)
|
||||||
|
- **类型**: 纯 HTML + JavaScript
|
||||||
|
- **端口**: 3002
|
||||||
|
- **启动**: `npm run serve` 或 `python3 -m http.server 3002`
|
||||||
|
- **特点**: 基础的 HTML 项目,用于测试最简单场景下的脚本注入
|
||||||
|
|
||||||
|
### 2. Vite 项目 (vite-project)
|
||||||
|
- **类型**: Vite + React
|
||||||
|
- **端口**: 3001
|
||||||
|
- **启动**: `npm run dev`
|
||||||
|
- **特点**: 现代化的 Vite 构建工具,单页应用
|
||||||
|
|
||||||
|
### 3. Next.js App Router (next-app-project)
|
||||||
|
- **类型**: Next.js 14 App Router
|
||||||
|
- **端口**: 3000
|
||||||
|
- **启动**: `npm run dev`
|
||||||
|
- **特点**: 最新的 Next.js 应用路由架构,支持 React Server Components
|
||||||
|
|
||||||
|
### 4. Next.js Pages Router (next-pages-project)
|
||||||
|
- **类型**: Next.js Pages Router
|
||||||
|
- **端口**: 3000
|
||||||
|
- **启动**: `npm run dev`
|
||||||
|
- **特点**: 传统的 Next.js Pages 路由架构
|
||||||
|
|
||||||
|
## 使用指南
|
||||||
|
|
||||||
|
### 快速开始
|
||||||
|
|
||||||
|
1. **选择一个测试项目**
|
||||||
|
```bash
|
||||||
|
cd test-projects/html-project # 或选择其他项目
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **安装依赖**(如果有 package.json)
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **启动项目**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# 或
|
||||||
|
npm run serve
|
||||||
|
# 或
|
||||||
|
python3 -m http.server 3002
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **使用 dev-inject 注入监控脚本**
|
||||||
|
```bash
|
||||||
|
# 在项目根目录运行
|
||||||
|
node ../bin/index.js install --remote http://localhost:9001/scripts/dev-monitor.js
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **测试功能**
|
||||||
|
- 打开浏览器访问项目
|
||||||
|
- 点击页面上的按钮触发各种错误
|
||||||
|
- 在浏览器控制台运行 DevMonitor 命令查看监控信息
|
||||||
|
|
||||||
|
### 测试命令
|
||||||
|
|
||||||
|
所有测试项目都支持以下 DevMonitor 命令:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 显示监控面板
|
||||||
|
DevMonitor.showPanel()
|
||||||
|
|
||||||
|
// 获取统计信息
|
||||||
|
DevMonitor.getStats()
|
||||||
|
|
||||||
|
// 清除错误记录
|
||||||
|
DevMonitor.clearErrors()
|
||||||
|
|
||||||
|
// 检查注入状态
|
||||||
|
window.__DEV_INJECT__
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
每个测试项目都包含以下功能:
|
||||||
|
|
||||||
|
- ✅ 触发同步错误
|
||||||
|
- ✅ 触发异步错误
|
||||||
|
- ✅ 错误记录和显示
|
||||||
|
- ✅ 清除错误功能
|
||||||
|
- ✅ DevMonitor 控制台命令
|
||||||
|
- ✅ 实时错误统计
|
||||||
|
- ✅ 美观的 UI 界面
|
||||||
|
|
||||||
|
## 项目对比
|
||||||
|
|
||||||
|
| 项目类型 | 构建工具 | 路由方式 | 启动速度 | 适用场景 |
|
||||||
|
|---------|---------|---------|---------|---------|
|
||||||
|
| HTML | 无 | 静态文件 | 最快 | 传统网站 |
|
||||||
|
| Vite | Vite | SPA | 快 | 现代 SPA 应用 |
|
||||||
|
| Next.js App | Next.js | App Router | 中 | 全栈应用(推荐) |
|
||||||
|
| Next.js Pages | Next.js | Pages Router | 中 | 传统 Next.js 应用 |
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **端口冲突**: 确保各个项目使用不同的端口
|
||||||
|
2. **依赖安装**: 每个项目需要单独安装依赖
|
||||||
|
3. **测试服务器**: 确保 dev-inject 的监控服务器 (localhost:9001) 正在运行
|
||||||
|
4. **框架检测**: dev-inject 会自动检测项目类型并应用相应的注入策略
|
||||||
|
|
||||||
|
## 问题排查
|
||||||
|
|
||||||
|
如果遇到问题:
|
||||||
|
|
||||||
|
1. 检查端口是否被占用
|
||||||
|
2. 确认所有依赖已正确安装
|
||||||
|
3. 查看浏览器控制台是否有错误
|
||||||
|
4. 确认监控服务器是否正在运行
|
||||||
|
5. 检查注入的脚本是否正确加载
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎添加更多测试项目或改进现有项目!
|
||||||
|
|
||||||
16
qiming-dev-inject/test-projects/html-project/.gitignore
vendored
Normal file
16
qiming-dev-inject/test-projects/html-project/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
41
qiming-dev-inject/test-projects/html-project/README.md
Normal file
41
qiming-dev-inject/test-projects/html-project/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# HTML 静态项目测试
|
||||||
|
|
||||||
|
这是一个用于测试 dev-inject 在纯 HTML 静态项目中的注入功能的示例项目。
|
||||||
|
|
||||||
|
## 启动项目
|
||||||
|
|
||||||
|
### 方式 1: 使用 Python
|
||||||
|
```bash
|
||||||
|
python3 -m http.server 3002
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式 2: 使用 serve
|
||||||
|
```bash
|
||||||
|
npx serve -s . -p 3002
|
||||||
|
```
|
||||||
|
|
||||||
|
项目将在 http://localhost:3002 运行。
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
1. 启动项目(任选上述方式之一)
|
||||||
|
2. 使用 dev-inject 注入监控脚本
|
||||||
|
3. 点击页面上的按钮触发各种错误类型
|
||||||
|
4. 在浏览器控制台运行 DevMonitor 命令查看监控信息
|
||||||
|
|
||||||
|
## DevMonitor 命令
|
||||||
|
|
||||||
|
- `DevMonitor.showPanel()` - 显示监控面板
|
||||||
|
- `DevMonitor.getStats()` - 获取统计信息
|
||||||
|
- `DevMonitor.clearErrors()` - 清除错误记录
|
||||||
|
- `window.__DEV_INJECT__` - 检查注入状态
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 触发同步错误
|
||||||
|
- 触发异步错误
|
||||||
|
- 触发引用错误
|
||||||
|
- 触发类型错误
|
||||||
|
- 实时统计错误数据
|
||||||
|
- 清除错误记录
|
||||||
|
|
||||||
31
qiming-dev-inject/test-projects/html-project/index.html
Normal file
31
qiming-dev-inject/test-projects/html-project/index.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HTML 静态项目 - dev-inject 测试</title>
|
||||||
|
<!-- DEV-INJECT-START -->
|
||||||
|
<script data-id="dev-inject-monitor">
|
||||||
|
(function() {
|
||||||
|
const remote = "https://testagent.xspaceagi.com/sdk/dev-monitor.js";
|
||||||
|
const separator = remote.includes('?') ? '&' : '?';
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = remote + separator + 't=' + Date.now();
|
||||||
|
script.dataset.id = 'dev-inject-monitor-script';
|
||||||
|
script.defer = true;
|
||||||
|
// 防止重复注入
|
||||||
|
if (!document.querySelector('[data-id="dev-inject-monitor-script"]')) {
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<!-- DEV-INJECT-END -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>HTML 静态项目测试</h1>
|
||||||
|
<p>测试 dev-inject 在纯 HTML 项目中的注入功能</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
17
qiming-dev-inject/test-projects/html-project/package.json
Normal file
17
qiming-dev-inject/test-projects/html-project/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "html-static-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "纯 HTML 静态项目测试 - dev-inject",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "python3 -m http.server 3002",
|
||||||
|
"serve": "npx serve -s . -p 3002"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"html",
|
||||||
|
"test",
|
||||||
|
"dev-inject"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
36
qiming-dev-inject/test-projects/next-app-project/.gitignore
vendored
Normal file
36
qiming-dev-inject/test-projects/next-app-project/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
32
qiming-dev-inject/test-projects/next-app-project/README.md
Normal file
32
qiming-dev-inject/test-projects/next-app-project/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Next.js App Router 测试项目
|
||||||
|
|
||||||
|
这是一个用于测试 dev-inject 框架感知注入功能的 Next.js App Router 项目。
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
项目将在 http://localhost:3000 运行。
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
1. 启动项目:`npm run dev`
|
||||||
|
2. 使用 dev-inject 注入监控脚本
|
||||||
|
3. 点击页面上的按钮触发各种错误
|
||||||
|
4. 在浏览器控制台运行 DevMonitor 命令查看监控信息
|
||||||
|
|
||||||
|
## DevMonitor 命令
|
||||||
|
|
||||||
|
- `DevMonitor.showPanel()` - 显示监控面板
|
||||||
|
- `DevMonitor.getStats()` - 获取统计信息
|
||||||
|
- `DevMonitor.clearErrors()` - 清除错误记录
|
||||||
|
- `window.__DEV_INJECT__` - 检查注入状态
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Next.js App Test - dev-inject',
|
||||||
|
description: '测试 dev-inject 框架感知注入功能',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<script dangerouslySetInnerHTML={{
|
||||||
|
__html: "(function() {\n const remote = \"https://testagent.xspaceagi.com/sdk/dev-monitor.js\";\n const separator = remote.includes('?') ? '&' : '?';\n const script = document.createElement('script');\n script.src = remote + separator + 't=' + Date.now();\n script.dataset.id = 'dev-inject-monitor-script';\n script.defer = true;\n // 防止重复注入\n if (!document.querySelector('[data-id=\"dev-inject-monitor-script\"]')) {\n document.head.appendChild(script);\n }\n })();"
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [errors, setErrors] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const triggerError = () => {
|
||||||
|
try {
|
||||||
|
throw new Error('测试错误 - Next.js App Router');
|
||||||
|
} catch (error) {
|
||||||
|
setErrors(prev => [...prev, (error as Error).message]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerAsyncError = () => {
|
||||||
|
Promise.reject(new Error('异步错误测试 - Next.js App'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearErrors = () => {
|
||||||
|
setErrors([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-gradient-to-br from-blue-500 to-purple-600 text-white">
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<header className="text-center mb-12">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">🟢 Next.js App Router Test</h1>
|
||||||
|
<p className="text-xl opacity-90">测试 dev-inject 框架感知注入功能</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<div className="bg-white/10 backdrop-blur-md rounded-xl p-6 mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">🚀 测试功能</h2>
|
||||||
|
<div className="flex flex-wrap gap-3 justify-center">
|
||||||
|
<button
|
||||||
|
onClick={triggerError}
|
||||||
|
className="px-6 py-3 bg-red-500 hover:bg-red-600 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
触发同步错误
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={triggerAsyncError}
|
||||||
|
className="px-6 py-3 bg-red-500 hover:bg-red-600 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
触发异步错误
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={clearErrors}
|
||||||
|
className="px-6 py-3 bg-gray-600 hover:bg-gray-700 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
清除错误
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/10 backdrop-blur-md rounded-xl p-6 mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">📊 错误记录</h2>
|
||||||
|
{errors.length === 0 ? (
|
||||||
|
<p className="text-gray-300 italic">暂无错误</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{errors.map((error, index) => (
|
||||||
|
<div key={index} className="bg-red-500/20 border-l-4 border-red-500 p-3 rounded font-mono text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/10 backdrop-blur-md rounded-xl p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">🔧 DevMonitor 检查</h2>
|
||||||
|
<p className="mb-3">在浏览器控制台中检查:</p>
|
||||||
|
<ul className="space-y-1 text-left max-w-md mx-auto">
|
||||||
|
<li><code className="bg-black/20 px-2 py-1 rounded">DevMonitor.showPanel()</code> - 显示监控面板</li>
|
||||||
|
<li><code className="bg-black/20 px-2 py-1 rounded">DevMonitor.getStats()</code> - 获取统计信息</li>
|
||||||
|
<li><code className="bg-black/20 px-2 py-1 rounded">window.__DEV_INJECT__</code> - 检查注入状态</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "next-app-test",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"next": "14.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"autoprefixer": "^10.0.1",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
|
||||||
36
qiming-dev-inject/test-projects/next-pages-project/.gitignore
vendored
Normal file
36
qiming-dev-inject/test-projects/next-pages-project/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
32
qiming-dev-inject/test-projects/next-pages-project/README.md
Normal file
32
qiming-dev-inject/test-projects/next-pages-project/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Next.js Pages Router 测试项目
|
||||||
|
|
||||||
|
这是一个用于测试 dev-inject 框架感知注入功能的 Next.js Pages Router 项目。
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
项目将在 http://localhost:3000 运行。
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
1. 启动项目:`npm run dev`
|
||||||
|
2. 使用 dev-inject 注入监控脚本
|
||||||
|
3. 点击页面上的按钮触发各种错误
|
||||||
|
4. 在浏览器控制台运行 DevMonitor 命令查看监控信息
|
||||||
|
|
||||||
|
## DevMonitor 命令
|
||||||
|
|
||||||
|
- `DevMonitor.showPanel()` - 显示监控面板
|
||||||
|
- `DevMonitor.getStats()` - 获取统计信息
|
||||||
|
- `DevMonitor.clearErrors()` - 清除错误记录
|
||||||
|
- `window.__DEV_INJECT__` - 检查注入状态
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "next-pages-test",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"next": "14.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import type { AppProps } from 'next/app'
|
||||||
|
import '../styles/globals.css'
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(function() {
|
||||||
|
const remote = "https://testagent.xspaceagi.com/sdk/dev-monitor.js";
|
||||||
|
const separator = remote.includes('?') ? '&' : '?';
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = remote + separator + 't=' + Date.now();
|
||||||
|
script.dataset.id = 'dev-inject-monitor-script';
|
||||||
|
script.defer = true;
|
||||||
|
// 防止重复注入
|
||||||
|
if (!document.querySelector('[data-id="dev-inject-monitor-script"]')) {
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
return <Component {...pageProps} />
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Html, Head, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
|
export default function Document() {
|
||||||
|
return (
|
||||||
|
<Html lang="zh-CN">
|
||||||
|
<Head>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* DEV-INJECT-START */}
|
||||||
|
{typeof window !== 'undefined' && (
|
||||||
|
<script dangerouslySetInnerHTML={{
|
||||||
|
__html: "(function() {\n const remote = \"https://testagent.xspaceagi.com/sdk/dev-monitor.js\";\n const separator = remote.includes('?') ? '&' : '?';\n const script = document.createElement('script');\n script.src = remote + separator + 't=' + Date.now();\n script.dataset.id = 'dev-inject-monitor-script';\n script.defer = true;\n // 防止重复注入\n if (!document.querySelector('[data-id=\"dev-inject-monitor-script\"]')) {\n document.head.appendChild(script);\n }\n })();"
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
{/* DEV-INJECT-END */}
|
||||||
|
</Head>
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import Head from 'next/head'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [errors, setErrors] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const triggerError = () => {
|
||||||
|
try {
|
||||||
|
throw new Error('测试错误 - Next.js Pages Router');
|
||||||
|
} catch (error) {
|
||||||
|
setErrors(prev => [...prev, (error as Error).message]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerAsyncError = () => {
|
||||||
|
Promise.reject(new Error('异步错误测试 - Next.js Pages'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearErrors = () => {
|
||||||
|
setErrors([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Next.js Pages Test - dev-inject</title>
|
||||||
|
<meta name="description" content="测试 dev-inject 在 Next.js Pages Router 中的注入功能" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
</Head>
|
||||||
|
<main className="min-h-screen bg-gradient-to-br from-green-500 to-teal-600 text-white">
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<header className="text-center mb-12">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">📄 Next.js Pages Router Test</h1>
|
||||||
|
<p className="text-xl opacity-90">测试 dev-inject 框架感知注入功能</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<div className="bg-white/10 backdrop-blur-md rounded-xl p-6 mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">🚀 测试功能</h2>
|
||||||
|
<div className="flex flex-wrap gap-3 justify-center">
|
||||||
|
<button
|
||||||
|
onClick={triggerError}
|
||||||
|
className="px-6 py-3 bg-red-500 hover:bg-red-600 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
触发同步错误
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={triggerAsyncError}
|
||||||
|
className="px-6 py-3 bg-red-500 hover:bg-red-600 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
触发异步错误
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={clearErrors}
|
||||||
|
className="px-6 py-3 bg-gray-600 hover:bg-gray-700 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
清除错误
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/10 backdrop-blur-md rounded-xl p-6 mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">📊 错误记录</h2>
|
||||||
|
{errors.length === 0 ? (
|
||||||
|
<p className="text-gray-300 italic">暂无错误</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{errors.map((error, index) => (
|
||||||
|
<div key={index} className="bg-red-500/20 border-l-4 border-red-500 p-3 rounded font-mono text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/10 backdrop-blur-md rounded-xl p-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">🔧 DevMonitor 检查</h2>
|
||||||
|
<p className="mb-3">在浏览器控制台中检查:</p>
|
||||||
|
<ul className="space-y-1 text-left max-w-md mx-auto">
|
||||||
|
<li><code className="bg-black/20 px-2 py-1 rounded">DevMonitor.showPanel()</code> - 显示监控面板</li>
|
||||||
|
<li><code className="bg-black/20 px-2 py-1 rounded">DevMonitor.getStats()</code> - 获取统计信息</li>
|
||||||
|
<li><code className="bg-black/20 px-2 py-1 rounded">window.__DEV_INJECT__</code> - 检查注入状态</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
|
|
||||||
25
qiming-dev-inject/test-projects/vite-project/.gitignore
vendored
Normal file
25
qiming-dev-inject/test-projects/vite-project/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
32
qiming-dev-inject/test-projects/vite-project/README.md
Normal file
32
qiming-dev-inject/test-projects/vite-project/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Vite React 测试项目
|
||||||
|
|
||||||
|
这是一个用于测试 dev-inject 框架感知注入功能的 Vite + React 项目。
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
项目将在 http://localhost:3001 运行。
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
1. 启动项目:`npm run dev`
|
||||||
|
2. 使用 dev-inject 注入监控脚本
|
||||||
|
3. 点击页面上的按钮触发各种错误
|
||||||
|
4. 在浏览器控制台运行 DevMonitor 命令查看监控信息
|
||||||
|
|
||||||
|
## DevMonitor 命令
|
||||||
|
|
||||||
|
- `DevMonitor.showPanel()` - 显示监控面板
|
||||||
|
- `DevMonitor.getStats()` - 获取统计信息
|
||||||
|
- `DevMonitor.clearErrors()` - 清除错误记录
|
||||||
|
- `window.__DEV_INJECT__` - 检查注入状态
|
||||||
|
|
||||||
16
qiming-dev-inject/test-projects/vite-project/index.html
Normal file
16
qiming-dev-inject/test-projects/vite-project/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite Test - dev-inject</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
21
qiming-dev-inject/test-projects/vite-project/package.json
Normal file
21
qiming-dev-inject/test-projects/vite-project/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-test-project",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.43",
|
||||||
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"vite": "^5.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// PostCSS 配置文件
|
||||||
|
// 此文件用于防止 Vite 向上查找父目录的 PostCSS 配置
|
||||||
|
// 本项目使用普通 CSS,不需要 PostCSS 插件
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
// 如果需要 PostCSS 插件,可以在这里添加
|
||||||
|
// 例如:autoprefixer, tailwindcss 等
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
119
qiming-dev-inject/test-projects/vite-project/src/App.css
Normal file
119
qiming-dev-inject/test-projects/vite-project/src/App.css
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger {
|
||||||
|
background: #f56565;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger:hover {
|
||||||
|
background: #e53e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary {
|
||||||
|
background: #718096;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary:hover {
|
||||||
|
background: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item {
|
||||||
|
background: rgba(245, 101, 101, 0.2);
|
||||||
|
border-left: 3px solid #f56565;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: left;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-errors {
|
||||||
|
color: #a0aec0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
73
qiming-dev-inject/test-projects/vite-project/src/App.jsx
Normal file
73
qiming-dev-inject/test-projects/vite-project/src/App.jsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [errors, setErrors] = useState([]);
|
||||||
|
|
||||||
|
const triggerError = () => {
|
||||||
|
try {
|
||||||
|
throw new Error('测试错误 - Vite 项目');
|
||||||
|
} catch (error) {
|
||||||
|
setErrors(prev => [...prev, error.message]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerAsyncError = () => {
|
||||||
|
Promise.reject(new Error('异步错误测试 - Vite'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearErrors = () => {
|
||||||
|
setErrors([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<div className="container">
|
||||||
|
<header>
|
||||||
|
<h1>🟣 Vite Test Project</h1>
|
||||||
|
<p>测试 dev-inject 框架感知注入功能</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="section">
|
||||||
|
<h2>🚀 测试功能</h2>
|
||||||
|
<div className="buttons">
|
||||||
|
<button onClick={triggerError} className="danger">
|
||||||
|
触发同步错误
|
||||||
|
</button>
|
||||||
|
<button onClick={triggerAsyncError} className="danger">
|
||||||
|
触发异步错误
|
||||||
|
</button>
|
||||||
|
<button onClick={clearErrors} className="secondary">
|
||||||
|
清除错误
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="section">
|
||||||
|
<h2>📊 错误记录</h2>
|
||||||
|
{errors.length === 0 ? (
|
||||||
|
<p className="no-errors">暂无错误</p>
|
||||||
|
) : (
|
||||||
|
<ul className="error-list">
|
||||||
|
{errors.map((error, index) => (
|
||||||
|
<li key={index} className="error-item">{error}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="section">
|
||||||
|
<h2>🔧 DevMonitor 检查</h2>
|
||||||
|
<p>在浏览器控制台中检查:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>DevMonitor.showPanel()</code> - 显示监控面板</li>
|
||||||
|
<li><code>DevMonitor.getStats()</code> - 获取统计信息</li>
|
||||||
|
<li><code>window.__DEV_INJECT__</code> - 检查注入状态</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
66
qiming-dev-inject/test-projects/vite-project/src/index.css
Normal file
66
qiming-dev-inject/test-projects/vite-project/src/index.css
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
qiming-dev-inject/test-projects/vite-project/src/main.jsx
Normal file
10
qiming-dev-inject/test-projects/vite-project/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.jsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
40
qiming-dev-inject/test-projects/vite-project/vite.config.js
Normal file
40
qiming-dev-inject/test-projects/vite-project/vite.config.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
|
||||||
|
// <!-- DEV-INJECT-START -->
|
||||||
|
{
|
||||||
|
name: 'dev-inject',
|
||||||
|
enforce: 'post', // 确保在 HTML 注入阶段最后执行
|
||||||
|
transformIndexHtml(html) {
|
||||||
|
if (!html.includes('data-id="dev-inject-monitor"')) {
|
||||||
|
return html.replace("</head>", `
|
||||||
|
<script data-id="dev-inject-monitor">
|
||||||
|
(function() {
|
||||||
|
const remote = "https://testagent.xspaceagi.com/sdk/dev-monitor.js";
|
||||||
|
const separator = remote.includes('?') ? '&' : '?';
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = remote + separator + 't=' + Date.now();
|
||||||
|
script.dataset.id = 'dev-inject-monitor-script';
|
||||||
|
script.defer = true;
|
||||||
|
// 防止重复注入
|
||||||
|
if (!document.querySelector('[data-id="dev-inject-monitor-script"]')) {
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
\n</head>`);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// <!-- DEV-INJECT-END -->
|
||||||
|
|
||||||
|
react()
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
port: 3001
|
||||||
|
}
|
||||||
|
})
|
||||||
388
qiming-dev-inject/test/e2e/e2e.test.js
Normal file
388
qiming-dev-inject/test/e2e/e2e.test.js
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import { test, describe } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { execSync, spawn } from 'child_process';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
describe('端到端测试', () => {
|
||||||
|
const testDir = join(__dirname, 'fixtures');
|
||||||
|
const cliPath = join(__dirname, '..', '..', 'bin', 'index.js');
|
||||||
|
const projectRoot = join(__dirname, '..', '..');
|
||||||
|
|
||||||
|
// 创建测试项目的基本结构
|
||||||
|
const testProjects = {
|
||||||
|
vite: {
|
||||||
|
name: 'test-vite-project',
|
||||||
|
files: {
|
||||||
|
'package.json': JSON.stringify({
|
||||||
|
"name": "test-vite-project",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": { "dev": "vite" },
|
||||||
|
"dependencies": { "react": "^18.2.0" }
|
||||||
|
}, null, 2),
|
||||||
|
'vite.config.js': `import { defineConfig } from 'vite'
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: []
|
||||||
|
})`,
|
||||||
|
'index.html': `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Vite Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>`,
|
||||||
|
'src/main.js': `import './app.js'`,
|
||||||
|
'src/app.js': `console.log('Vite app loaded');
|
||||||
|
document.getElementById('app').innerHTML = '<h1>Vite App</h1>';`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'next-app': {
|
||||||
|
name: 'test-next-app',
|
||||||
|
files: {
|
||||||
|
'package.json': JSON.stringify({
|
||||||
|
"name": "test-next-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": { "dev": "next dev" }
|
||||||
|
}, null, 2),
|
||||||
|
'next.config.js': 'module.exports = {}',
|
||||||
|
'app/layout.tsx': `import './globals.css'
|
||||||
|
export default function RootLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)}`,
|
||||||
|
'app/page.tsx': `export default function Home() {
|
||||||
|
return <h1>Next.js App</h1>;
|
||||||
|
}`,
|
||||||
|
'app/globals.css': 'body { font-family: Arial, sans-serif; }'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'html-static': {
|
||||||
|
name: 'test-html-project',
|
||||||
|
files: {
|
||||||
|
'index.html': `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Static HTML</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Static HTML Project</h1>
|
||||||
|
<p>This is a static HTML project.</p>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function createProject(projectName, files) {
|
||||||
|
const projectPath = join(testDir, projectName);
|
||||||
|
mkdirSync(projectPath, { recursive: true });
|
||||||
|
|
||||||
|
for (const [filePath, content] of Object.entries(files)) {
|
||||||
|
const fullPath = join(projectPath, filePath);
|
||||||
|
const dir = dirname(fullPath);
|
||||||
|
|
||||||
|
if (!existsSync(dir)) {
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(fullPath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupProject(projectPath) {
|
||||||
|
if (existsSync(projectPath)) {
|
||||||
|
rmSync(projectPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCliInProject(projectPath, args, options = {}) {
|
||||||
|
try {
|
||||||
|
const result = execSync(`node ${cliPath} ${args}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
cwd: projectPath,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
timeout: options.timeout || 10000
|
||||||
|
});
|
||||||
|
return { success: true, output: result };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: error.stdout || error.message,
|
||||||
|
code: error.status,
|
||||||
|
signal: error.signal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test.before(() => {
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(() => {
|
||||||
|
// 清理所有测试项目
|
||||||
|
Object.keys(testProjects).forEach(projectName => {
|
||||||
|
const projectPath = join(testDir, testProjects[projectName].name);
|
||||||
|
cleanupProject(projectPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Vite 项目完整流程', () => {
|
||||||
|
test('应该完成 Vite 项目的完整注入流程', () => {
|
||||||
|
const projectPath = createProject(testProjects.vite.name, testProjects.vite.files);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 检测项目类型
|
||||||
|
const detectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run --verbose');
|
||||||
|
assert.strictEqual(detectResult.success, true);
|
||||||
|
assert.match(detectResult.output, /检测到项目类型: vite/);
|
||||||
|
|
||||||
|
// 2. 执行实际注入
|
||||||
|
const injectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework');
|
||||||
|
assert.strictEqual(injectResult.success, true);
|
||||||
|
assert.match(injectResult.output, /框架注入成功/);
|
||||||
|
|
||||||
|
// 3. 验证配置文件被修改
|
||||||
|
const viteConfigPath = join(projectPath, 'vite.config.js');
|
||||||
|
assert(existsSync(viteConfigPath));
|
||||||
|
const configContent = readFileSync(viteConfigPath, 'utf8');
|
||||||
|
assert.match(configContent, /dev-inject-plugin/);
|
||||||
|
assert.match(configContent, /scripts\/monitor\.js/);
|
||||||
|
|
||||||
|
// 4. 验证原始配置保留
|
||||||
|
assert.match(configContent, /defineConfig/);
|
||||||
|
|
||||||
|
// 5. 执行卸载
|
||||||
|
const uninstallResult = runCliInProject(projectPath, 'uninstall --framework');
|
||||||
|
assert.strictEqual(uninstallResult.success, true);
|
||||||
|
|
||||||
|
// 6. 验证插件被移除
|
||||||
|
const cleanedContent = readFileSync(viteConfigPath, 'utf8');
|
||||||
|
assert(!cleanedContent.includes('dev-inject-plugin'));
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
cleanupProject(projectPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理不同的脚本类型', () => {
|
||||||
|
const projectPath = createProject(testProjects.vite.name, testProjects.vite.files);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 测试 HTTP URL
|
||||||
|
const httpResult = runCliInProject(projectPath, 'install --remote=https://cdn.example.com/monitor.js --framework --dry-run');
|
||||||
|
assert.strictEqual(httpResult.success, true);
|
||||||
|
assert.match(httpResult.output, /检测到项目类型: vite/);
|
||||||
|
|
||||||
|
// 测试本地路径
|
||||||
|
const localResult = runCliInProject(projectPath, 'install --remote=/assets/dev-monitor.js --framework --dry-run');
|
||||||
|
assert.strictEqual(localResult.success, true);
|
||||||
|
assert.match(localResult.output, /检测到项目类型: vite/);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
cleanupProject(projectPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Next.js 项目完整流程', () => {
|
||||||
|
test('应该完成 Next.js App Router 的完整注入流程', () => {
|
||||||
|
const projectPath = createProject(testProjects['next-app'].name, testProjects['next-app'].files);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 检测项目类型
|
||||||
|
const detectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run --verbose');
|
||||||
|
assert.strictEqual(detectResult.success, true);
|
||||||
|
assert.match(detectResult.output, /检测到项目类型: next-app/);
|
||||||
|
|
||||||
|
// 2. 执行实际注入
|
||||||
|
const injectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework');
|
||||||
|
assert.strictEqual(injectResult.success, true);
|
||||||
|
assert.match(injectResult.output, /框架注入成功/);
|
||||||
|
|
||||||
|
// 3. 验证 layout 文件被修改
|
||||||
|
const layoutPath = join(projectPath, 'app/layout.tsx');
|
||||||
|
assert(existsSync(layoutPath));
|
||||||
|
const layoutContent = readFileSync(layoutPath, 'utf8');
|
||||||
|
assert.match(layoutContent, /process\.env\.NODE_ENV === 'development'/);
|
||||||
|
assert.match(layoutContent, /scripts\/monitor\.js/);
|
||||||
|
|
||||||
|
// 4. 验证原始内容保留
|
||||||
|
assert.match(layoutContent, /RootLayout/);
|
||||||
|
assert.match(layoutContent, /children/);
|
||||||
|
|
||||||
|
// 5. 执行卸载
|
||||||
|
const uninstallResult = runCliInProject(projectPath, 'uninstall --framework');
|
||||||
|
assert.strictEqual(uninstallResult.success, true);
|
||||||
|
|
||||||
|
// 6. 验证注入内容被移除
|
||||||
|
const cleanedContent = readFileSync(layoutPath, 'utf8');
|
||||||
|
assert(!cleanedContent.includes('process.env.NODE_ENV'));
|
||||||
|
assert(!cleanedContent.includes('scripts/monitor.js'));
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
cleanupProject(projectPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('静态 HTML 项目完整流程', () => {
|
||||||
|
test('应该完成静态 HTML 项目的完整注入流程', () => {
|
||||||
|
const projectPath = createProject(testProjects['html-static'].name, testProjects['html-static'].files);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 检测项目类型
|
||||||
|
const detectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run --verbose');
|
||||||
|
assert.strictEqual(detectResult.success, true);
|
||||||
|
assert.match(detectResult.output, /检测到项目类型: html-static/);
|
||||||
|
|
||||||
|
// 2. 执行实际注入
|
||||||
|
const injectResult = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework');
|
||||||
|
assert.strictEqual(injectResult.success, true);
|
||||||
|
assert.match(injectResult.output, /成功处理 \d+ 个文件/);
|
||||||
|
|
||||||
|
// 3. 验证 HTML 文件被修改
|
||||||
|
const htmlPath = join(projectPath, 'index.html');
|
||||||
|
assert(existsSync(htmlPath));
|
||||||
|
const htmlContent = readFileSync(htmlPath, 'utf8');
|
||||||
|
assert.match(htmlContent, /<script src="\/scripts\/monitor\.js"><\/script>/);
|
||||||
|
assert.match(htmlContent, /<!-- injected by dev-inject -->/);
|
||||||
|
|
||||||
|
// 4. 验证原始内容保留
|
||||||
|
assert.match(htmlContent, /Static HTML Project/);
|
||||||
|
|
||||||
|
// 5. 执行卸载
|
||||||
|
const uninstallResult = runCliInProject(projectPath, 'uninstall');
|
||||||
|
assert.strictEqual(uninstallResult.success, true);
|
||||||
|
assert.match(uninstallResult.output, /成功清理 \d+ 个文件/);
|
||||||
|
|
||||||
|
// 6. 验证脚本被移除
|
||||||
|
const cleanedContent = readFileSync(htmlPath, 'utf8');
|
||||||
|
assert(!cleanedContent.includes('scripts/monitor.js'));
|
||||||
|
assert(!cleanedContent.includes('injected by dev-inject'));
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
cleanupProject(projectPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('回退机制测试', () => {
|
||||||
|
test('应该在不支持的项目中回退到传统模式', () => {
|
||||||
|
// 创建一个不支持的空项目
|
||||||
|
const projectPath = join(testDir, 'unsupported-project');
|
||||||
|
mkdirSync(projectPath, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run');
|
||||||
|
|
||||||
|
// 应该回退到传统模式并找到 HTML 文件
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /回退到传统 HTML 注入模式/);
|
||||||
|
} finally {
|
||||||
|
cleanupProject(projectPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('并发测试', () => {
|
||||||
|
test('应该处理多个并发命令', async () => {
|
||||||
|
const projectPath = createProject(testProjects.vite.name, testProjects.vite.files);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建多个并发命令
|
||||||
|
const commands = [
|
||||||
|
runCliInProject(projectPath, 'install --remote=/scripts/monitor1.js --framework --dry-run'),
|
||||||
|
runCliInProject(projectPath, 'install --remote=/scripts/monitor2.js --framework --dry-run'),
|
||||||
|
runCliInProject(projectPath, 'install --remote=/scripts/monitor3.js --framework --dry-run')
|
||||||
|
];
|
||||||
|
|
||||||
|
// 等待所有命令完成
|
||||||
|
const results = await Promise.all(commands);
|
||||||
|
|
||||||
|
// 所有命令都应该成功
|
||||||
|
results.forEach(result => {
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
cleanupProject(projectPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('性能测试', () => {
|
||||||
|
test('应该在合理时间内完成大型项目操作', () => {
|
||||||
|
// 创建一个大项目(很多文件)
|
||||||
|
const projectPath = join(testDir, 'large-project');
|
||||||
|
mkdirSync(projectPath, { recursive: true });
|
||||||
|
|
||||||
|
// 创建大量 HTML 文件
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
writeFileSync(join(projectPath, `file${i}.html`), `<html><head><title>File ${i}</title></head><body></body></html>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 Vite 配置
|
||||||
|
writeFileSync(join(projectPath, 'vite.config.js'), 'module.exports = { plugins: [] }');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const result = runCliInProject(projectPath, 'install --remote=/scripts/monitor.js --framework --dry-run', {
|
||||||
|
timeout: 30000 // 30秒超时
|
||||||
|
});
|
||||||
|
const endTime = Date.now();
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert(endTime - startTime < 10000, '大项目操作应该在 10 秒内完成');
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
cleanupProject(projectPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('真实场景模拟', () => {
|
||||||
|
test('应该模拟真实的开发工作流', () => {
|
||||||
|
const projectPath = createProject(testProjects.vite.name, testProjects.vite.files);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 初始注入
|
||||||
|
const initialInject = runCliInProject(projectPath, 'install --remote=/scripts/dev-monitor.js --framework');
|
||||||
|
assert.strictEqual(initialInject.success, true);
|
||||||
|
|
||||||
|
// 2. 重新注入(应该检测到已存在)
|
||||||
|
const reInject = runCliInProject(projectPath, 'install --remote=/scripts/dev-monitor.js --framework --dry-run');
|
||||||
|
assert.strictEqual(reInject.success, true);
|
||||||
|
|
||||||
|
// 3. 更换脚本
|
||||||
|
const switchScript = runCliInProject(projectPath, 'install --remote=/scripts/new-monitor.js --framework --dry-run');
|
||||||
|
assert.strictEqual(switchScript.success, true);
|
||||||
|
|
||||||
|
// 4. 卸载
|
||||||
|
const uninstall = runCliInProject(projectPath, 'uninstall --framework');
|
||||||
|
assert.strictEqual(uninstall.success, true);
|
||||||
|
|
||||||
|
// 5. 清理后重新注入
|
||||||
|
const cleanReinject = runCliInProject(projectPath, 'install --remote=/scripts/dev-monitor.js --framework --dry-run');
|
||||||
|
assert.strictEqual(cleanReinject.success, true);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
cleanupProject(projectPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
313
qiming-dev-inject/test/integration/cli.test.js
Normal file
313
qiming-dev-inject/test/integration/cli.test.js
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
import { test, describe } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
describe('CLI 功能集成测试', () => {
|
||||||
|
const testDir = join(__dirname, 'fixtures');
|
||||||
|
const cliPath = join(__dirname, '..', '..', 'bin', 'index.js');
|
||||||
|
const projectRoot = join(__dirname, '..', '..');
|
||||||
|
|
||||||
|
test.before(() => {
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(() => {
|
||||||
|
// 清理测试文件
|
||||||
|
const filesToClean = [
|
||||||
|
'vite.config.js',
|
||||||
|
'next.config.js',
|
||||||
|
'app/layout.tsx',
|
||||||
|
'pages/_document.tsx',
|
||||||
|
'test.html',
|
||||||
|
'public/index.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
filesToClean.forEach(file => {
|
||||||
|
const filePath = join(testDir, file);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
rmSync(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const dirsToClean = ['app', 'pages', 'public'];
|
||||||
|
dirsToClean.forEach(dir => {
|
||||||
|
const dirPath = join(testDir, dir);
|
||||||
|
if (existsSync(dirPath)) {
|
||||||
|
rmSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function runCli(args, options = {}) {
|
||||||
|
try {
|
||||||
|
const result = execSync(`node ${cliPath} ${args}`, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
cwd: options.cwd || projectRoot,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
return { success: true, output: result };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: error.stdout || error.message,
|
||||||
|
code: error.status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('基本 CLI 命令', () => {
|
||||||
|
test('应该显示帮助信息', () => {
|
||||||
|
const result = runCli('--help');
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /dev-inject.*开发环境脚本注入工具/);
|
||||||
|
assert.match(result.output, /--framework.*使用框架感知注入模式/);
|
||||||
|
assert.match(result.output, /--remote.*脚本地址/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该显示版本信息', () => {
|
||||||
|
const result = runCli('--version');
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /dev-inject v\d+\.\d+\.\d+/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理无效参数', () => {
|
||||||
|
const result = runCli('--invalid-option');
|
||||||
|
|
||||||
|
// 应该仍然成功(忽略未知选项)
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该在缺少参数时显示错误', () => {
|
||||||
|
const result = runCli('install');
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, false);
|
||||||
|
assert.match(result.output, /install 命令需要 --remote 参数/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('框架感知命令', () => {
|
||||||
|
test('应该检测到 Vite 项目', () => {
|
||||||
|
// 创建 Vite 项目
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = { plugins: [] }');
|
||||||
|
|
||||||
|
const result = runCli('install --remote=/scripts/test.js --framework --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /检测到项目类型: vite/);
|
||||||
|
assert.match(result.output, /DRY RUN/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该检测到 Next.js App Router', () => {
|
||||||
|
// 创建 Next.js App Router
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||||
|
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() {}');
|
||||||
|
|
||||||
|
const result = runCli('install --remote=/scripts/test.js --framework --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /检测到项目类型: next-app/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该检测到 Next.js Pages Router', () => {
|
||||||
|
// 创建 Next.js Pages Router
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||||
|
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() {}');
|
||||||
|
|
||||||
|
const result = runCli('install --remote=/scripts/test.js --framework --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /检测到项目类型: next-pages/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该检测到 Create React App', () => {
|
||||||
|
// Create React App
|
||||||
|
mkdirSync(join(testDir, 'public'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'public', 'index.html'), '<html></html>');
|
||||||
|
|
||||||
|
const result = runCli('install --remote=/scripts/test.js --framework --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /检测到项目类型: create-react-app/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该检测到 HTML 项目', () => {
|
||||||
|
writeFileSync(join(testDir, 'index.html'), '<html></html>');
|
||||||
|
|
||||||
|
const result = runCli('install --remote=/scripts/test.js --framework --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /检测到项目类型: html-static/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('传统注入模式', () => {
|
||||||
|
test('应该注入到 HTML 文件', () => {
|
||||||
|
// 创建 HTML 文件
|
||||||
|
const htmlContent = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
writeFileSync(join(testDir, 'test.html'), htmlContent);
|
||||||
|
|
||||||
|
const result = runCli('install --remote=/scripts/test.js --file=./test.html --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /DRY RUN.*将注入脚本到.*test\.html/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该查找多个 HTML 文件', () => {
|
||||||
|
// 创建多个 HTML 文件
|
||||||
|
writeFileSync(join(testDir, 'index.html'), '<html></html>');
|
||||||
|
writeFileSync(join(testDir, 'test.html'), '<html></html>');
|
||||||
|
|
||||||
|
const result = runCli('install --remote=/scripts/test.js --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /找到 \d+ 个 HTML 文件/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('卸载功能', () => {
|
||||||
|
test('应该支持传统卸载', () => {
|
||||||
|
writeFileSync(join(testDir, 'test.html'), '<html></html>');
|
||||||
|
|
||||||
|
const result = runCli('uninstall --file=./test.html --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /卸载脚本/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该支持框架卸载', () => {
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = { plugins: [] }');
|
||||||
|
|
||||||
|
const result = runCli('uninstall --framework --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
// 框架卸载的输出可能不同
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('详细输出模式', () => {
|
||||||
|
test('应该显示详细的注入信息', () => {
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = { plugins: [] }');
|
||||||
|
|
||||||
|
const result = runCli('install --remote=/scripts/test.js --framework --dry-run --verbose', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert.match(result.output, /脚本类型:/);
|
||||||
|
assert.match(result.output, /脚本标签:/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('错误处理', () => {
|
||||||
|
test('应该处理不存在的文件', () => {
|
||||||
|
const result = runCli('install --remote=/scripts/test.js --file=./nonexistent.html', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, false);
|
||||||
|
assert.match(result.output, /未找到 HTML 文件/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理权限问题', () => {
|
||||||
|
// 创建只读目录(在某些系统上可能不支持)
|
||||||
|
try {
|
||||||
|
mkdirSync(join(testDir, 'readonly'), { recursive: true, mode: 0o444 });
|
||||||
|
|
||||||
|
const result = runCli('install --remote=/scripts/test.js --file=./readonly/test.html', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
// 应该有某种错误处理
|
||||||
|
assert.ok(true);
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略权限相关的错误
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('参数验证', () => {
|
||||||
|
test('应该验证远程 URL 格式', () => {
|
||||||
|
const result = runCli('install --remote=invalid-url --framework --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
// 应该仍然接受,验证在实际注入时进行
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理空参数', () => {
|
||||||
|
const result = runCli('install --remote= --framework --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理长参数', () => {
|
||||||
|
const longRemote = 'https://example.com/' + 'a'.repeat(100) + '/script.js';
|
||||||
|
const result = runCli(`install --remote=${longRemote} --framework --dry-run`, {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('性能测试', () => {
|
||||||
|
test('应该在合理时间内完成', () => {
|
||||||
|
// 创建一些文件
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
writeFileSync(join(testDir, `test${i}.html`), '<html></html>');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
const result = runCli('install --remote=/scripts/test.js --dry-run', {
|
||||||
|
cwd: testDir
|
||||||
|
});
|
||||||
|
const endTime = Date.now();
|
||||||
|
|
||||||
|
assert.strictEqual(result.success, true);
|
||||||
|
assert(endTime - startTime < 5000, 'CLI 命令应该在 5 秒内完成');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
1
qiming-dev-inject/test/integration/fixtures/index.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
1
qiming-dev-inject/test/integration/fixtures/test0.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test0.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
1
qiming-dev-inject/test/integration/fixtures/test1.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test1.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
1
qiming-dev-inject/test/integration/fixtures/test2.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test2.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
1
qiming-dev-inject/test/integration/fixtures/test3.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test3.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
1
qiming-dev-inject/test/integration/fixtures/test4.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test4.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
1
qiming-dev-inject/test/integration/fixtures/test5.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test5.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
1
qiming-dev-inject/test/integration/fixtures/test6.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test6.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
1
qiming-dev-inject/test/integration/fixtures/test7.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test7.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
1
qiming-dev-inject/test/integration/fixtures/test8.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test8.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
1
qiming-dev-inject/test/integration/fixtures/test9.html
Normal file
1
qiming-dev-inject/test/integration/fixtures/test9.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
409
qiming-dev-inject/test/integration/smartInject.test.js
Normal file
409
qiming-dev-inject/test/integration/smartInject.test.js
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
import { test, describe } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { existsSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// 导入要测试的模块
|
||||||
|
import { smartInject } from '../../lib/framework-inject.js';
|
||||||
|
|
||||||
|
describe('框架注入集成测试', () => {
|
||||||
|
const testDir = join(__dirname, 'fixtures');
|
||||||
|
|
||||||
|
test.before(() => {
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(() => {
|
||||||
|
// 清理所有测试文件
|
||||||
|
const filesToClean = [
|
||||||
|
'vite.config.js',
|
||||||
|
'vite.config.ts',
|
||||||
|
'next.config.js',
|
||||||
|
'app/layout.tsx',
|
||||||
|
'app/layout.js',
|
||||||
|
'pages/_document.tsx',
|
||||||
|
'pages/_document.js',
|
||||||
|
'test.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
filesToClean.forEach(file => {
|
||||||
|
const filePath = join(testDir, file);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
rmSync(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const dirsToClean = ['app', 'pages'];
|
||||||
|
dirsToClean.forEach(dir => {
|
||||||
|
const dirPath = join(testDir, dir);
|
||||||
|
if (existsSync(dirPath)) {
|
||||||
|
rmSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Vite 项目注入', () => {
|
||||||
|
test('应该成功注入到 Vite 配置文件', () => {
|
||||||
|
// 创建 Vite 配置文件
|
||||||
|
const viteConfig = `import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 3000
|
||||||
|
}
|
||||||
|
})`;
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), viteConfig);
|
||||||
|
|
||||||
|
// 执行注入
|
||||||
|
const result = smartInject({
|
||||||
|
remote: 'http://localhost:9000/dev-monitor.js',
|
||||||
|
dryRun: true,
|
||||||
|
verbose: true
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该检查已存在的插件', () => {
|
||||||
|
// 创建已有插件的 Vite 配置
|
||||||
|
const viteConfigWithPlugin = `import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
// dev-inject-plugin
|
||||||
|
{
|
||||||
|
name: 'dev-inject-plugin',
|
||||||
|
transformIndexHtml(html) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return html.replace('</head>', '<script src="/existing.js"></script></head>');
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
port: 3000
|
||||||
|
}
|
||||||
|
})`;
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), viteConfigWithPlugin);
|
||||||
|
|
||||||
|
// 执行注入(应该检测到已存在)
|
||||||
|
const result = smartInject({
|
||||||
|
remote: 'http://localhost:9000/dev-monitor.js',
|
||||||
|
dryRun: true
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理没有 plugins 数组的情况', () => {
|
||||||
|
// 创建没有 plugins 的配置
|
||||||
|
const simpleConfig = `export default {
|
||||||
|
server: {
|
||||||
|
port: 3000
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), simpleConfig);
|
||||||
|
|
||||||
|
const result = smartInject({
|
||||||
|
remote: '/scripts/monitor.js',
|
||||||
|
dryRun: true
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理 TypeScript 配置文件', () => {
|
||||||
|
const tsConfig = `import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()]
|
||||||
|
})`;
|
||||||
|
writeFileSync(join(testDir, 'vite.config.ts'), tsConfig);
|
||||||
|
|
||||||
|
const result = smartInject({
|
||||||
|
remote: '/scripts/monitor.js',
|
||||||
|
dryRun: true
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Next.js App Router 注入', () => {
|
||||||
|
test('应该成功注入到 app/layout.tsx', () => {
|
||||||
|
// 创建 Next.js App Router 结构
|
||||||
|
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||||
|
|
||||||
|
const layoutContent = `import type { Metadata } from 'next'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Next.js App',
|
||||||
|
description: 'Generated by create next app',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}`;
|
||||||
|
writeFileSync(join(testDir, 'app', 'layout.tsx'), layoutContent);
|
||||||
|
|
||||||
|
const result = smartInject({
|
||||||
|
remote: 'http://localhost:9000/dev-monitor.js',
|
||||||
|
dryRun: true
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理 app/layout.js', () => {
|
||||||
|
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||||
|
|
||||||
|
const jsLayout = `import './globals.css'
|
||||||
|
|
||||||
|
export default function RootLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}`;
|
||||||
|
writeFileSync(join(testDir, 'app', 'layout.js'), jsLayout);
|
||||||
|
|
||||||
|
const result = smartInject({
|
||||||
|
remote: '/scripts/monitor.js',
|
||||||
|
dryRun: true
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Next.js Pages Router 注入', () => {
|
||||||
|
test('应该成功注入到 pages/_document.tsx', () => {
|
||||||
|
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||||
|
|
||||||
|
const documentContent = `import { Html, Head, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
|
export default function Document() {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
</Head>
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}`;
|
||||||
|
writeFileSync(join(testDir, 'pages', '_document.tsx'), documentContent);
|
||||||
|
|
||||||
|
const result = smartInject({
|
||||||
|
remote: 'http://localhost:9000/dev-monitor.js',
|
||||||
|
dryRun: true
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理 pages/_document.js', () => {
|
||||||
|
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||||
|
|
||||||
|
const jsDocument = `const { Html, Head, Main, NextScript } = require('next/document')
|
||||||
|
|
||||||
|
export default function Document() {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head>
|
||||||
|
<title>My App</title>
|
||||||
|
</Head>
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}`;
|
||||||
|
writeFileSync(join(testDir, 'pages', '_document.js'), jsDocument);
|
||||||
|
|
||||||
|
const result = smartInject({
|
||||||
|
remote: '/scripts/monitor.js',
|
||||||
|
dryRun: true
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('混合模式处理', () => {
|
||||||
|
test('应该优先注入到 app/layout', () => {
|
||||||
|
// 创建混合模式结构
|
||||||
|
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||||
|
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||||
|
|
||||||
|
writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() { return <div>App Layout</div>; }');
|
||||||
|
writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() { return <div>Document</div>; }');
|
||||||
|
|
||||||
|
const result = smartInject({
|
||||||
|
remote: '/scripts/monitor.js',
|
||||||
|
dryRun: true
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('错误处理', () => {
|
||||||
|
test('应该处理不支持的框架类型', () => {
|
||||||
|
writeFileSync(join(testDir, 'index.html'), '<html></html>');
|
||||||
|
|
||||||
|
try {
|
||||||
|
smartInject({
|
||||||
|
remote: '/scripts/monitor.js',
|
||||||
|
dryRun: false
|
||||||
|
}, testDir);
|
||||||
|
assert.fail('应该抛出错误');
|
||||||
|
} catch (error) {
|
||||||
|
assert.match(error.message, /不支持的项目类型/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理配置文件不存在的情况', () => {
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {}');
|
||||||
|
// 不创建 layout 文件
|
||||||
|
|
||||||
|
try {
|
||||||
|
smartInject({
|
||||||
|
remote: '/scripts/monitor.js',
|
||||||
|
dryRun: false
|
||||||
|
}, testDir);
|
||||||
|
assert.fail('应该抛出错误');
|
||||||
|
} catch (error) {
|
||||||
|
assert.match(error.message, /未找到/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理无效的配置文件', () => {
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), 'invalid javascript content');
|
||||||
|
|
||||||
|
try {
|
||||||
|
smartInject({
|
||||||
|
remote: '/scripts/monitor.js',
|
||||||
|
dryRun: false
|
||||||
|
}, testDir);
|
||||||
|
assert.fail('应该抛出错误');
|
||||||
|
} catch (error) {
|
||||||
|
assert.ok(error.message.length > 0); // 应该有某种错误信息
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('实际文件修改测试', () => {
|
||||||
|
test('应该实际修改 Vite 配置文件', () => {
|
||||||
|
const originalConfig = `import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 3000
|
||||||
|
}
|
||||||
|
})`;
|
||||||
|
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), originalConfig);
|
||||||
|
|
||||||
|
// 执行实际注入
|
||||||
|
const result = smartInject({
|
||||||
|
remote: '/scripts/monitor.js',
|
||||||
|
dryRun: false,
|
||||||
|
verbose: false
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
|
// 验证文件被修改
|
||||||
|
const modifiedContent = readFileSync(join(testDir, 'vite.config.js'), 'utf8');
|
||||||
|
assert.match(modifiedContent, /dev-inject-plugin/);
|
||||||
|
assert.match(modifiedContent, /scripts\/monitor\.js/);
|
||||||
|
|
||||||
|
// 验证原始内容保留
|
||||||
|
assert.match(modifiedContent, /defineConfig/);
|
||||||
|
assert.match(modifiedContent, /react\(\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该能够移除注入的内容', () => {
|
||||||
|
// 先注入
|
||||||
|
smartInject({
|
||||||
|
remote: '/scripts/monitor.js',
|
||||||
|
dryRun: false
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
// 然后移除
|
||||||
|
const result = removeInjection({
|
||||||
|
dryRun: false,
|
||||||
|
verbose: false
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
|
||||||
|
// 验证插件被移除
|
||||||
|
const cleanedContent = readFileSync(join(testDir, 'vite.config.js'), 'utf8');
|
||||||
|
assert(!cleanedContent.includes('dev-inject-plugin'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('不同脚本类型', () => {
|
||||||
|
test('应该处理 HTTP URL', () => {
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), 'export default { plugins: [] }');
|
||||||
|
|
||||||
|
const result = smartInject({
|
||||||
|
remote: 'https://cdn.example.com/monitor.js',
|
||||||
|
dryRun: true
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理本地路径', () => {
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), 'export default { plugins: [] }');
|
||||||
|
|
||||||
|
const result = smartInject({
|
||||||
|
remote: '/assets/dev-monitor.js',
|
||||||
|
dryRun: true
|
||||||
|
}, testDir);
|
||||||
|
|
||||||
|
assert.strictEqual(result, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
14
qiming-dev-inject/test/package.json
Normal file
14
qiming-dev-inject/test/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test",
|
||||||
|
"test:unit": "node --test test/unit/*.test.js",
|
||||||
|
"test:integration": "node --test test/integration/*.test.js",
|
||||||
|
"test:e2e": "node --test test/e2e/*.test.js",
|
||||||
|
"test:watch": "node --test --watch",
|
||||||
|
"test:coverage": "node --test --experimental-test-coverage"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"node:test": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
180
qiming-dev-inject/test/test-runner.js
Normal file
180
qiming-dev-inject/test/test-runner.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
console.log('🧪 dev-inject 测试运行器\n');
|
||||||
|
console.log('=' .repeat(50));
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const testDir = new URL('.', import.meta.url).pathname;
|
||||||
|
|
||||||
|
// 颜要在项目根目录运行
|
||||||
|
const projectRoot = process.cwd();
|
||||||
|
const cliPath = join(projectRoot, 'bin', 'index.js');
|
||||||
|
const testPackagePath = join(projectRoot, 'test', 'package.json');
|
||||||
|
|
||||||
|
// 检查测试环境
|
||||||
|
function checkTestEnvironment() {
|
||||||
|
if (!existsSync(cliPath)) {
|
||||||
|
console.error('❌ CLI 路径不存在:', cliPath);
|
||||||
|
console.log('请确保在 dev-inject 项目根目录运行此脚本');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(testPackagePath)) {
|
||||||
|
console.error('❌ 测试配置不存在:', testPackagePath);
|
||||||
|
console.log('正在创建测试配置...');
|
||||||
|
execSync('npm init -y', { cwd: join(projectRoot, 'test'), stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行指定类型的测试
|
||||||
|
function runTests(testType) {
|
||||||
|
console.log(`\n🚀 运行 ${testType.toUpperCase()} 测试...\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const testCommand = `node --test test/${testType}/*.test.js`;
|
||||||
|
execSync(testCommand, {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: 'inherit',
|
||||||
|
timeout: 60000 // 60秒超时
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`\n❌ ${testType.toUpperCase()} 测试失败`);
|
||||||
|
if (error.signal === 'SIGTERM') {
|
||||||
|
console.log(' 原因: 测试超时');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行特定测试
|
||||||
|
function runSpecificTest(testPath) {
|
||||||
|
console.log(`\n🎯 运行特定测试: ${testPath}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`node --test ${testPath}`, {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: 'inherit',
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`\n❌ 测试 ${testPath} 失败`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成测试报告
|
||||||
|
function generateTestReport(results) {
|
||||||
|
const totalTests = Object.keys(results).length;
|
||||||
|
const passedTests = Object.values(results).filter(Boolean).length;
|
||||||
|
const failedTests = totalTests - passedTests;
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(50));
|
||||||
|
console.log('📊 测试报告汇总');
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
console.log(`✅ 通过: ${passedTests}/${totalTests}`);
|
||||||
|
console.log(`❌ 失败: ${failedTests}/${totalTests}`);
|
||||||
|
console.log(`📈 成功率: ${Math.round((passedTests / totalTests) * 100)}%`);
|
||||||
|
|
||||||
|
if (passedTests === totalTests) {
|
||||||
|
console.log('\n🎉 所有测试都通过了!dev-inject 功能正常!');
|
||||||
|
} else {
|
||||||
|
console.log('\n⚠️ 有部分测试失败,请检查相关功能。');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🔍 测试覆盖范围:');
|
||||||
|
console.log(' 📦 单元测试 - 核心函数和逻辑');
|
||||||
|
console.log(' 🔧 集成测试 - 模块间协作');
|
||||||
|
console.log(' 🌐 端到端测试 - 完整工作流');
|
||||||
|
console.log(' 📱 框架支持 - Vite、Next.js、HTML');
|
||||||
|
|
||||||
|
return passedTests === totalTests;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
// 检查测试环境
|
||||||
|
checkTestEnvironment();
|
||||||
|
|
||||||
|
// 解析命令行参数
|
||||||
|
if (args.includes('--help') || args.includes('-h')) {
|
||||||
|
console.log(`
|
||||||
|
🧪 dev-inject 测试运行器
|
||||||
|
|
||||||
|
用法:
|
||||||
|
node test/test-runner.js [选项] [测试类型/文件]
|
||||||
|
|
||||||
|
选项:
|
||||||
|
--help, -h 显示帮助信息
|
||||||
|
--unit 运行单元测试
|
||||||
|
--integration 运行集成测试
|
||||||
|
--e2e 运行端到端测试
|
||||||
|
--all 运行所有测试
|
||||||
|
--verbose 显示详细输出
|
||||||
|
--watch 监听模式(需要 node --test 支持)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
node test/test-runner.js --unit # 只运行单元测试
|
||||||
|
node test/test-runner.js --integration # 只运行集成测试
|
||||||
|
node test/test-runner.js --e2e # 只运行端到端测试
|
||||||
|
node test/test-runner.js --all # 运行所有测试
|
||||||
|
node test/test-runner.js test/unit/parseArgs.test.js # 运行特定测试文件
|
||||||
|
|
||||||
|
测试类型:
|
||||||
|
unit 单元测试(核心函数)
|
||||||
|
integration 集成测试(模块协作)
|
||||||
|
e2e 端到端测试(完整流程)
|
||||||
|
`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
if (args.includes('--all')) {
|
||||||
|
// 运行所有测试
|
||||||
|
console.log('🎯 运行所有测试套件...');
|
||||||
|
results.unit = runTests('unit');
|
||||||
|
results.integration = runTests('integration');
|
||||||
|
results.e2e = runTests('e2e');
|
||||||
|
} else if (args.includes('--unit')) {
|
||||||
|
// 运行单元测试
|
||||||
|
results.unit = runTests('unit');
|
||||||
|
} else if (args.includes('--integration')) {
|
||||||
|
// 运行集成测试
|
||||||
|
results.integration = runTests('integration');
|
||||||
|
} else if (args.includes('--e2e')) {
|
||||||
|
// 运行端到端测试
|
||||||
|
results.e2e = runTests('e2e');
|
||||||
|
} else if (args.some(arg => arg.endsWith('.test.js'))) {
|
||||||
|
// 运行特定测试文件
|
||||||
|
const testFile = args.find(arg => arg.endsWith('.test.js'));
|
||||||
|
const success = runSpecificTest(testFile);
|
||||||
|
process.exit(success ? 0 : 1);
|
||||||
|
} else {
|
||||||
|
// 默认运行快速测试
|
||||||
|
console.log('🚀 运行快速测试套件...');
|
||||||
|
results.unit = runTests('unit');
|
||||||
|
|
||||||
|
if (results.unit) {
|
||||||
|
console.log('\n✅ 单元测试通过,继续运行集成测试...');
|
||||||
|
results.integration = runTests('integration');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.unit && results.integration) {
|
||||||
|
console.log('\n✅ 基础测试通过,可以运行完整测试:');
|
||||||
|
console.log(' node test/test-runner.js --all');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成报告
|
||||||
|
const allPassed = generateTestReport(results);
|
||||||
|
process.exit(allPassed ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
160
qiming-dev-inject/test/unit/detectProjectType.test.js
Normal file
160
qiming-dev-inject/test/unit/detectProjectType.test.js
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { test, describe } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// 导入要测试的模块
|
||||||
|
import { detectProjectType } from '../../lib/framework-inject.js';
|
||||||
|
|
||||||
|
describe('框架检测功能', () => {
|
||||||
|
const testDir = join(__dirname, 'fixtures');
|
||||||
|
|
||||||
|
// 测试前设置
|
||||||
|
test.before(() => {
|
||||||
|
// 确保测试目录存在
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试后清理
|
||||||
|
test.after(() => {
|
||||||
|
// 清理测试文件
|
||||||
|
const filesToClean = [
|
||||||
|
'vite.config.js',
|
||||||
|
'vite.config.ts',
|
||||||
|
'next.config.js',
|
||||||
|
'app/layout.tsx',
|
||||||
|
'pages/_document.tsx',
|
||||||
|
'public/index.html',
|
||||||
|
'index.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
filesToClean.forEach(file => {
|
||||||
|
const filePath = join(testDir, file);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
rmSync(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Vite 项目检测', () => {
|
||||||
|
test('应该检测到 Vite 项目 (js)', () => {
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'vite');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该检测到 Vite 项目 (ts)', () => {
|
||||||
|
writeFileSync(join(testDir, 'vite.config.ts'), 'export default {};');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'vite');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('优先检测 Vite 而不是其他配置', () => {
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||||
|
writeFileSync(join(testDir, 'public/index.html'), '<html></html>');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'vite');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Next.js App Router 检测', () => {
|
||||||
|
test('应该检测到 Next.js App Router', () => {
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||||
|
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() {}');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'next-app');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该检测到 Next.js App Router (js)', () => {
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||||
|
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'app', 'layout.js'), 'export default function Layout() {}');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'next-app');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Next.js Pages Router 检测', () => {
|
||||||
|
test('应该检测到 Next.js Pages Router', () => {
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||||
|
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() {}');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'next-pages');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该检测到 Next.js Pages Router (js)', () => {
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||||
|
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'pages', '_document.js'), 'export default function Document() {}');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'next-pages');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Next.js 混合模式检测', () => {
|
||||||
|
test('应该检测到 Next.js 混合模式', () => {
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||||
|
mkdirSync(join(testDir, 'app'), { recursive: true });
|
||||||
|
mkdirSync(join(testDir, 'pages'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() {}');
|
||||||
|
writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() {}');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'next-hybrid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Create React App 检测', () => {
|
||||||
|
test('应该检测到 Create React App', () => {
|
||||||
|
mkdirSync(join(testDir, 'public'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'public', 'index.html'), '<html></html>');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'create-react-app');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('HTML 静态项目检测', () => {
|
||||||
|
test('应该检测到 HTML 静态项目', () => {
|
||||||
|
writeFileSync(join(testDir, 'index.html'), '<html></html>');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'html-static');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('未知项目检测', () => {
|
||||||
|
test('应该返回 unknown 对于空目录', () => {
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该返回 unknown 对于不存在的目录', () => {
|
||||||
|
const type = detectProjectType('/non-existent-directory');
|
||||||
|
assert.strictEqual(type, 'unknown');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('优先级检测', () => {
|
||||||
|
test('Next.js 优先于 HTML', () => {
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||||
|
writeFileSync(join(testDir, 'index.html'), '<html></html>');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'next-app'); // 默认是 app 模式
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Vite 优先于 Next.js', () => {
|
||||||
|
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
|
||||||
|
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
|
||||||
|
const type = detectProjectType(testDir);
|
||||||
|
assert.strictEqual(type, 'vite');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
1
qiming-dev-inject/test/unit/fixtures/app/layout.js
Normal file
1
qiming-dev-inject/test/unit/fixtures/app/layout.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default function Layout() {}
|
||||||
1
qiming-dev-inject/test/unit/fixtures/index.html
Normal file
1
qiming-dev-inject/test/unit/fixtures/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
1
qiming-dev-inject/test/unit/fixtures/pages/_document.js
Normal file
1
qiming-dev-inject/test/unit/fixtures/pages/_document.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default function Document() {}
|
||||||
1
qiming-dev-inject/test/unit/fixtures/test.html
Normal file
1
qiming-dev-inject/test/unit/fixtures/test.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<html></html>
|
||||||
161
qiming-dev-inject/test/unit/parseArgs.test.js
Normal file
161
qiming-dev-inject/test/unit/parseArgs.test.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { test, describe } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
|
||||||
|
// 导入要测试的模块
|
||||||
|
import { parseArgs } from '../../lib/args.js';
|
||||||
|
|
||||||
|
describe('CLI 参数解析', () => {
|
||||||
|
describe('基本命令解析', () => {
|
||||||
|
test('应该解析 install 命令', () => {
|
||||||
|
const result = parseArgs(['install', '--remote', '/scripts/test.js']);
|
||||||
|
assert.strictEqual(result.command, 'install');
|
||||||
|
assert.strictEqual(result.options.remote, '/scripts/test.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该解析 uninstall 命令', () => {
|
||||||
|
const result = parseArgs(['uninstall']);
|
||||||
|
assert.strictEqual(result.command, 'uninstall');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理空参数', () => {
|
||||||
|
const result = parseArgs([]);
|
||||||
|
assert.strictEqual(result.command, null);
|
||||||
|
assert.deepStrictEqual(result.options, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--remote 参数解析', () => {
|
||||||
|
test('应该解析 --remote=value 格式', () => {
|
||||||
|
const result = parseArgs(['install', '--remote=http://localhost:9000/monitor.js']);
|
||||||
|
assert.strictEqual(result.options.remote, 'http://localhost:9000/monitor.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该解析 --remote value 格式', () => {
|
||||||
|
const result = parseArgs(['install', '--remote', '/scripts/dev-monitor.js']);
|
||||||
|
assert.strictEqual(result.options.remote, '/scripts/dev-monitor.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理多个 --remote 参数(使用最后一个)', () => {
|
||||||
|
const result = parseArgs(['install', '--remote', '/first.js', '--remote', '/second.js']);
|
||||||
|
assert.strictEqual(result.options.remote, '/second.js');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--file 参数解析', () => {
|
||||||
|
test('应该解析 --file=value 格式', () => {
|
||||||
|
const result = parseArgs(['install', '--remote=/test.js', '--file=public/index.html']);
|
||||||
|
assert.strictEqual(result.options.file, 'public/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该解析 --file value 格式', () => {
|
||||||
|
const result = parseArgs(['install', '--remote=/test.js', '--file', './public/index.html']);
|
||||||
|
assert.strictEqual(result.options.file, './public/index.html');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--framework 参数解析', () => {
|
||||||
|
test('应该解析 --framework 选项', () => {
|
||||||
|
const result = parseArgs(['install', '--remote=/test.js', '--framework']);
|
||||||
|
assert.strictEqual(result.options.framework, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该解析 -f 简写选项', () => {
|
||||||
|
const result = parseArgs(['install', '--remote=/test.js', '-f']);
|
||||||
|
assert.strictEqual(result.options.framework, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--dry-run 参数解析', () => {
|
||||||
|
test('应该解析 --dry-run 选项', () => {
|
||||||
|
const result = parseArgs(['install', '--remote=/test.js', '--dry-run']);
|
||||||
|
assert.strictEqual(result.options.dryRun, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('--verbose 参数解析', () => {
|
||||||
|
test('应该解析 --verbose 选项', () => {
|
||||||
|
const result = parseArgs(['install', '--remote=/test.js', '--verbose']);
|
||||||
|
assert.strictEqual(result.options.verbose, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('复合选项解析', () => {
|
||||||
|
test('应该解析多个选项', () => {
|
||||||
|
const result = parseArgs([
|
||||||
|
'install',
|
||||||
|
'--remote', 'http://localhost:9000/monitor.js',
|
||||||
|
'--framework',
|
||||||
|
'--dry-run',
|
||||||
|
'--verbose',
|
||||||
|
'--file', './test.html'
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.strictEqual(result.command, 'install');
|
||||||
|
assert.strictEqual(result.options.remote, 'http://localhost:9000/monitor.js');
|
||||||
|
assert.strictEqual(result.options.framework, true);
|
||||||
|
assert.strictEqual(result.options.dryRun, true);
|
||||||
|
assert.strictEqual(result.options.verbose, true);
|
||||||
|
assert.strictEqual(result.options.file, './test.html');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('错误处理', () => {
|
||||||
|
test('install 命令缺少 --remote 参数应该抛出错误', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
parseArgs(['install']);
|
||||||
|
}, /install 命令需要 --remote 参数/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uninstall 命令不需要 --remote 参数', () => {
|
||||||
|
const result = parseArgs(['uninstall']);
|
||||||
|
assert.strictEqual(result.command, 'uninstall');
|
||||||
|
assert.deepStrictEqual(result.options, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('无效参数处理', () => {
|
||||||
|
test('应该忽略未知参数', () => {
|
||||||
|
// 使用 mock console.warn 来捕获警告
|
||||||
|
const originalWarn = console.warn;
|
||||||
|
let warnCalled = false;
|
||||||
|
console.warn = () => { warnCalled = true; };
|
||||||
|
|
||||||
|
const result = parseArgs(['install', '--remote=/test.js', '--unknown-option']);
|
||||||
|
|
||||||
|
console.warn = originalWarn;
|
||||||
|
|
||||||
|
// 应该仍然正确解析已知参数
|
||||||
|
assert.strictEqual(result.options.remote, '/test.js');
|
||||||
|
// 应该标记未知参数被忽略(通过 console.warn)
|
||||||
|
assert.ok(warnCalled); // 如果有警告输出则测试通过
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('参数顺序无关性', () => {
|
||||||
|
test('应该正确处理不同顺序的参数', () => {
|
||||||
|
const result1 = parseArgs(['install', '--remote=/test.js', '--framework', '--dry-run']);
|
||||||
|
const result2 = parseArgs(['install', '--dry-run', '--framework', '--remote=/test.js']);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result1.options, result2.options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('边界情况', () => {
|
||||||
|
test('应该处理空的 --remote 值', () => {
|
||||||
|
const result = parseArgs(['install', '--remote', '']);
|
||||||
|
assert.strictEqual(result.options.remote, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理包含特殊字符的路径', () => {
|
||||||
|
const complexPath = '/path with spaces/file.js';
|
||||||
|
const result = parseArgs(['install', '--remote', complexPath]);
|
||||||
|
assert.strictEqual(result.options.remote, complexPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理长参数值', () => {
|
||||||
|
const longValue = 'a'.repeat(1000);
|
||||||
|
const result = parseArgs(['install', '--remote', longValue]);
|
||||||
|
assert.strictEqual(result.options.remote, longValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
304
qiming-dev-inject/test/unit/utils.test.js
Normal file
304
qiming-dev-inject/test/unit/utils.test.js
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import { test, describe } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// 导入要测试的模块
|
||||||
|
import {
|
||||||
|
parseRemoteType,
|
||||||
|
generateScriptTag,
|
||||||
|
lookupFiles,
|
||||||
|
injectScriptToHtml,
|
||||||
|
removeInjectedScripts
|
||||||
|
} from '../../lib/utils.js';
|
||||||
|
|
||||||
|
describe('工具函数', () => {
|
||||||
|
const testDir = join(__dirname, 'fixtures');
|
||||||
|
|
||||||
|
test.before(() => {
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(() => {
|
||||||
|
const filesToClean = [
|
||||||
|
'test1.html',
|
||||||
|
'test2.html',
|
||||||
|
'subdir/test3.html',
|
||||||
|
'subdir/test4.html'
|
||||||
|
];
|
||||||
|
|
||||||
|
filesToClean.forEach(file => {
|
||||||
|
const filePath = join(testDir, file);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
rmSync(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const subDir = join(testDir, 'subdir');
|
||||||
|
if (existsSync(subDir)) {
|
||||||
|
rmSync(subDir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseRemoteType', () => {
|
||||||
|
test('应该识别 HTTP URL', () => {
|
||||||
|
assert.strictEqual(parseRemoteType('http://localhost:9000/monitor.js'), 'url');
|
||||||
|
assert.strictEqual(parseRemoteType('http://example.com/script.js'), 'url');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该识别 HTTPS URL', () => {
|
||||||
|
assert.strictEqual(parseRemoteType('https://cdn.example.com/monitor.js'), 'url');
|
||||||
|
assert.strictEqual(parseRemoteType('https://localhost:3000/dev-monitor.js'), 'url');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该识别绝对路径', () => {
|
||||||
|
assert.strictEqual(parseRemoteType('/scripts/monitor.js'), 'absolute-path');
|
||||||
|
assert.strictEqual(parseRemoteType('/assets/dev-monitor.js'), 'absolute-path');
|
||||||
|
assert.strictEqual(parseRemoteType('/path/to/script.js'), 'absolute-path');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理相对路径', () => {
|
||||||
|
try {
|
||||||
|
parseRemoteType('./script.js');
|
||||||
|
assert.fail('应该抛出错误');
|
||||||
|
} catch (error) {
|
||||||
|
assert.match(error.message, /不支持的远程路径格式/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理空字符串', () => {
|
||||||
|
try {
|
||||||
|
parseRemoteType('');
|
||||||
|
assert.fail('应该抛出错误');
|
||||||
|
} catch (error) {
|
||||||
|
assert.match(error.message, /不支持的远程路径格式/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateScriptTag', () => {
|
||||||
|
test('应该生成 URL 类型的脚本标签', () => {
|
||||||
|
const tag = generateScriptTag('http://localhost:9000/monitor.js', 'url');
|
||||||
|
assert.strictEqual(tag, '<script src="http://localhost:9000/monitor.js"></script>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该生成绝对路径类型的脚本标签', () => {
|
||||||
|
const tag = generateScriptTag('/scripts/monitor.js', 'absolute-path');
|
||||||
|
assert.strictEqual(tag, '<script src="/scripts/monitor.js"></script>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理不同的路径格式', () => {
|
||||||
|
const testCases = [
|
||||||
|
'https://cdn.example.com/monitor.js',
|
||||||
|
'http://localhost:3000/dev-monitor.js',
|
||||||
|
'/assets/scripts/dev-monitor.js',
|
||||||
|
'/scripts/monitor.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(path => {
|
||||||
|
const type = parseRemoteType(path);
|
||||||
|
const tag = generateScriptTag(path, type);
|
||||||
|
assert.match(tag, /^<script src="[^"]*"><\/script>$/);
|
||||||
|
assert(tag.includes(path));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('lookupFiles', () => {
|
||||||
|
test('应该查找指定目录中的 HTML 文件', () => {
|
||||||
|
// 创建测试文件
|
||||||
|
writeFileSync(join(testDir, 'test1.html'), '<html></html>');
|
||||||
|
writeFileSync(join(testDir, 'test2.html'), '<html></html>');
|
||||||
|
mkdirSync(join(testDir, 'subdir'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'subdir', 'test3.html'), '<html></html>');
|
||||||
|
|
||||||
|
const files = lookupFiles(testDir);
|
||||||
|
|
||||||
|
// 应该找到所有 HTML 文件
|
||||||
|
assert(files.length >= 3);
|
||||||
|
assert(files.some(f => f.includes('test1.html')));
|
||||||
|
assert(files.some(f => f.includes('test2.html')));
|
||||||
|
assert(files.some(f => f.includes('test3.html')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该优先返回 index.html', () => {
|
||||||
|
// 创建测试文件
|
||||||
|
writeFileSync(join(testDir, 'test.html'), '<html></html>');
|
||||||
|
writeFileSync(join(testDir, 'index.html'), '<html></html>');
|
||||||
|
|
||||||
|
const files = lookupFiles(testDir);
|
||||||
|
|
||||||
|
// 应该优先返回 index.html
|
||||||
|
assert.strictEqual(files.length, 1);
|
||||||
|
assert(files[0].includes('index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理空目录', () => {
|
||||||
|
const files = lookupFiles(testDir);
|
||||||
|
assert.deepStrictEqual(files, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理不存在的目录', () => {
|
||||||
|
const files = lookupFiles('/non-existent-directory');
|
||||||
|
assert.deepStrictEqual(files, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该跳过 node_modules 目录', () => {
|
||||||
|
// 创建测试文件和 node_modules 目录
|
||||||
|
writeFileSync(join(testDir, 'test.html'), '<html></html>');
|
||||||
|
mkdirSync(join(testDir, 'node_modules'), { recursive: true });
|
||||||
|
writeFileSync(join(testDir, 'node_modules', 'index.html'), '<html></html>');
|
||||||
|
|
||||||
|
const files = lookupFiles(testDir);
|
||||||
|
|
||||||
|
// 不应该包含 node_modules 中的文件
|
||||||
|
assert(files.length === 1);
|
||||||
|
assert(files[0].includes('test.html'));
|
||||||
|
assert(!files.some(f => f.includes('node_modules')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('injectScriptToHtml', () => {
|
||||||
|
test('应该注入脚本到 HTML 的 </head> 标签之前', () => {
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
const scriptTag = '<script src="/scripts/monitor.js"></script>';
|
||||||
|
const result = injectScriptToHtml(html, scriptTag);
|
||||||
|
|
||||||
|
assert.match(result, /<script src="\/scripts\/monitor\.js"><\/script>\s*<\/head>/);
|
||||||
|
assert.match(result, /dev-inject/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该添加 dev-inject 标识注释', () => {
|
||||||
|
const html = `<html><head></head><body></body></html>`;
|
||||||
|
const scriptTag = '<script src="/test.js"></script>';
|
||||||
|
const result = injectScriptToHtml(html, scriptTag);
|
||||||
|
|
||||||
|
assert.match(result, /<!-- injected by dev-inject -->/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理没有 </head> 标签的情况', () => {
|
||||||
|
const html = `<html><head><title>Test</title></head><body>content</body></html>`;
|
||||||
|
const scriptTag = '<script src="/test.js"></script>';
|
||||||
|
const result = injectScriptToHtml(html, scriptTag);
|
||||||
|
|
||||||
|
assert.match(result, /<script src="\/test\.js"><\/script>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理没有 <head> 标签的情况', () => {
|
||||||
|
const html = `<html><body>content</body></html>`;
|
||||||
|
const scriptTag = '<script src="/test.js"></script>';
|
||||||
|
const result = injectScriptToHtml(html, scriptTag);
|
||||||
|
|
||||||
|
assert.match(result, /<script src="\/test\.js"><\/script>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理空 HTML', () => {
|
||||||
|
const html = '';
|
||||||
|
const scriptTag = '<script src="/test.js"></script>';
|
||||||
|
const result = injectScriptToHtml(html, scriptTag);
|
||||||
|
|
||||||
|
assert.match(result, /<script src="\/test\.js"><\/script>/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeInjectedScripts', () => {
|
||||||
|
test('应该移除注入的脚本和注释', () => {
|
||||||
|
const htmlWithInjection = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Test</title>
|
||||||
|
<script src="/scripts/monitor.js"></script> <!-- injected by dev-inject -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
const result = removeInjectedScripts(htmlWithInjection);
|
||||||
|
|
||||||
|
assert.match(result, /<head>\s*<meta charset="UTF-8">\s*<title>Test<\/title>\s*<\/head>/);
|
||||||
|
assert(!result.includes('dev-monitor.js'));
|
||||||
|
assert(!result.includes('injected by dev-inject'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理多个注入的脚本', () => {
|
||||||
|
const htmlWithMultiple = `<html><head>
|
||||||
|
<script src="/script1.js"></script> <!-- injected by dev-inject -->
|
||||||
|
<title>Test</title>
|
||||||
|
<script src="/script2.js"></script> <!-- injected by dev-inject -->
|
||||||
|
</head></html>`;
|
||||||
|
|
||||||
|
const result = removeInjectedScripts(htmlWithMultiple);
|
||||||
|
|
||||||
|
assert(!result.includes('script1.js'));
|
||||||
|
assert(!result.includes('script2.js'));
|
||||||
|
assert(!result.includes('injected by dev-inject'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该保留非注入的脚本', () => {
|
||||||
|
const htmlWithMixed = `<html><head>
|
||||||
|
<script src="/normal-script.js"></script>
|
||||||
|
<script src="/injected-script.js"></script> <!-- injected by dev-inject -->
|
||||||
|
<title>Test</title>
|
||||||
|
</head></html>`;
|
||||||
|
|
||||||
|
const result = removeInjectedScripts(htmlWithMixed);
|
||||||
|
|
||||||
|
assert(result.includes('normal-script.js'));
|
||||||
|
assert(!result.includes('injected-script.js'));
|
||||||
|
assert(!result.includes('injected by dev-inject'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理没有注入脚本的 HTML', () => {
|
||||||
|
const normalHtml = `<html><head><title>Test</title></head><body>content</body></html>`;
|
||||||
|
const result = removeInjectedScripts(normalHtml);
|
||||||
|
|
||||||
|
assert.strictEqual(result, normalHtml);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('边界情况和错误处理', () => {
|
||||||
|
test('应该处理特殊字符', () => {
|
||||||
|
const html = '<html><head><title>Test & "Special" Characters</title></head></html>';
|
||||||
|
const scriptTag = '<script src="/path/with spaces/script.js"></script>';
|
||||||
|
const result = injectScriptToHtml(html, scriptTag);
|
||||||
|
|
||||||
|
assert(result.includes('script.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理非常长的 HTML', () => {
|
||||||
|
const longHtml = '<html><head>' + '<meta charset="UTF-8">'.repeat(1000) + '</head></html>';
|
||||||
|
const scriptTag = '<script src="/test.js"></script>';
|
||||||
|
const result = injectScriptToHtml(longHtml, scriptTag);
|
||||||
|
|
||||||
|
assert(result.length > longHtml.length);
|
||||||
|
assert(result.includes('test.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该处理格式不规范的 HTML', () => {
|
||||||
|
const malformedHtml = '<html><HEAD><title>Test</Title></HEAD><BODY>content</BODY></HTML>';
|
||||||
|
const scriptTag = '<script src="/test.js"></script>';
|
||||||
|
const result = injectScriptToHtml(malformedHtml, scriptTag);
|
||||||
|
|
||||||
|
assert(result.includes('test.js'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user