From 9e9486b7c24ace69a34277d57cf9f6d505f9eef0 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 1 Jun 2026 12:53:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4qiming-dev-inject?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + qiming-dev-inject/.gitignore | 76 ++ qiming-dev-inject/.npmignore | 73 ++ qiming-dev-inject/EXAMPLES.md | 149 +++ qiming-dev-inject/LICENSE | 22 + qiming-dev-inject/README.md | 257 ++++++ qiming-dev-inject/bin/index.js | 54 ++ qiming-dev-inject/lib/args.js | 48 + qiming-dev-inject/lib/code-cleaner.js | 112 +++ qiming-dev-inject/lib/file-finder.js | 133 +++ qiming-dev-inject/lib/framework-inject.js | 850 ++++++++++++++++++ qiming-dev-inject/lib/help.js | 115 +++ qiming-dev-inject/lib/inject.js | 206 +++++ qiming-dev-inject/lib/utils.js | 275 ++++++ qiming-dev-inject/package.json | 50 ++ qiming-dev-inject/scripts/dev-monitor.js | 223 +++++ qiming-dev-inject/scripts/test-projects.js | 534 +++++++++++ qiming-dev-inject/test-projects/README.md | 124 +++ .../test-projects/html-project/.gitignore | 16 + .../test-projects/html-project/README.md | 41 + .../test-projects/html-project/index.html | 31 + .../test-projects/html-project/package.json | 17 + .../test-projects/next-app-project/.gitignore | 36 + .../test-projects/next-app-project/README.md | 32 + .../next-app-project/app/globals.css | 3 + .../next-app-project/app/layout.tsx | 32 + .../next-app-project/app/page.tsx | 85 ++ .../next-app-project/next.config.js | 4 + .../next-app-project/package.json | 25 + .../next-app-project/postcss.config.js | 7 + .../next-app-project/tailwind.config.js | 12 + .../next-app-project/tsconfig.json | 28 + .../next-pages-project/.gitignore | 36 + .../next-pages-project/README.md | 32 + .../next-pages-project/next.config.js | 5 + .../next-pages-project/package.json | 22 + .../next-pages-project/pages/_app.tsx | 97 ++ .../next-pages-project/pages/_document.tsx | 24 + .../next-pages-project/pages/index.tsx | 92 ++ .../next-pages-project/postcss.config.js | 7 + .../next-pages-project/styles/globals.css | 17 + .../next-pages-project/tailwind.config.js | 12 + .../next-pages-project/tsconfig.json | 24 + .../test-projects/vite-project/.gitignore | 25 + .../test-projects/vite-project/README.md | 32 + .../test-projects/vite-project/index.html | 16 + .../test-projects/vite-project/package.json | 21 + .../vite-project/postcss.config.js | 12 + .../test-projects/vite-project/src/App.css | 119 +++ .../test-projects/vite-project/src/App.jsx | 73 ++ .../test-projects/vite-project/src/index.css | 66 ++ .../test-projects/vite-project/src/main.jsx | 10 + .../test-projects/vite-project/vite.config.js | 40 + qiming-dev-inject/test/e2e/e2e.test.js | 388 ++++++++ .../test/integration/cli.test.js | 313 +++++++ .../test/integration/fixtures/index.html | 1 + .../test/integration/fixtures/test0.html | 1 + .../test/integration/fixtures/test1.html | 1 + .../test/integration/fixtures/test2.html | 1 + .../test/integration/fixtures/test3.html | 1 + .../test/integration/fixtures/test4.html | 1 + .../test/integration/fixtures/test5.html | 1 + .../test/integration/fixtures/test6.html | 1 + .../test/integration/fixtures/test7.html | 1 + .../test/integration/fixtures/test8.html | 1 + .../test/integration/fixtures/test9.html | 1 + .../test/integration/smartInject.test.js | 409 +++++++++ qiming-dev-inject/test/package.json | 14 + qiming-dev-inject/test/test-runner.js | 180 ++++ .../test/unit/detectProjectType.test.js | 160 ++++ .../test/unit/fixtures/app/layout.js | 1 + .../test/unit/fixtures/index.html | 1 + .../test/unit/fixtures/pages/_document.js | 1 + .../test/unit/fixtures/test.html | 1 + qiming-dev-inject/test/unit/parseArgs.test.js | 161 ++++ qiming-dev-inject/test/unit/utils.test.js | 304 +++++++ 76 files changed, 6397 insertions(+) create mode 100644 qiming-dev-inject/.gitignore create mode 100644 qiming-dev-inject/.npmignore create mode 100644 qiming-dev-inject/EXAMPLES.md create mode 100644 qiming-dev-inject/LICENSE create mode 100644 qiming-dev-inject/README.md create mode 100644 qiming-dev-inject/bin/index.js create mode 100644 qiming-dev-inject/lib/args.js create mode 100644 qiming-dev-inject/lib/code-cleaner.js create mode 100644 qiming-dev-inject/lib/file-finder.js create mode 100644 qiming-dev-inject/lib/framework-inject.js create mode 100644 qiming-dev-inject/lib/help.js create mode 100644 qiming-dev-inject/lib/inject.js create mode 100644 qiming-dev-inject/lib/utils.js create mode 100644 qiming-dev-inject/package.json create mode 100644 qiming-dev-inject/scripts/dev-monitor.js create mode 100644 qiming-dev-inject/scripts/test-projects.js create mode 100644 qiming-dev-inject/test-projects/README.md create mode 100644 qiming-dev-inject/test-projects/html-project/.gitignore create mode 100644 qiming-dev-inject/test-projects/html-project/README.md create mode 100644 qiming-dev-inject/test-projects/html-project/index.html create mode 100644 qiming-dev-inject/test-projects/html-project/package.json create mode 100644 qiming-dev-inject/test-projects/next-app-project/.gitignore create mode 100644 qiming-dev-inject/test-projects/next-app-project/README.md create mode 100644 qiming-dev-inject/test-projects/next-app-project/app/globals.css create mode 100644 qiming-dev-inject/test-projects/next-app-project/app/layout.tsx create mode 100644 qiming-dev-inject/test-projects/next-app-project/app/page.tsx create mode 100644 qiming-dev-inject/test-projects/next-app-project/next.config.js create mode 100644 qiming-dev-inject/test-projects/next-app-project/package.json create mode 100644 qiming-dev-inject/test-projects/next-app-project/postcss.config.js create mode 100644 qiming-dev-inject/test-projects/next-app-project/tailwind.config.js create mode 100644 qiming-dev-inject/test-projects/next-app-project/tsconfig.json create mode 100644 qiming-dev-inject/test-projects/next-pages-project/.gitignore create mode 100644 qiming-dev-inject/test-projects/next-pages-project/README.md create mode 100644 qiming-dev-inject/test-projects/next-pages-project/next.config.js create mode 100644 qiming-dev-inject/test-projects/next-pages-project/package.json create mode 100644 qiming-dev-inject/test-projects/next-pages-project/pages/_app.tsx create mode 100644 qiming-dev-inject/test-projects/next-pages-project/pages/_document.tsx create mode 100644 qiming-dev-inject/test-projects/next-pages-project/pages/index.tsx create mode 100644 qiming-dev-inject/test-projects/next-pages-project/postcss.config.js create mode 100644 qiming-dev-inject/test-projects/next-pages-project/styles/globals.css create mode 100644 qiming-dev-inject/test-projects/next-pages-project/tailwind.config.js create mode 100644 qiming-dev-inject/test-projects/next-pages-project/tsconfig.json create mode 100644 qiming-dev-inject/test-projects/vite-project/.gitignore create mode 100644 qiming-dev-inject/test-projects/vite-project/README.md create mode 100644 qiming-dev-inject/test-projects/vite-project/index.html create mode 100644 qiming-dev-inject/test-projects/vite-project/package.json create mode 100644 qiming-dev-inject/test-projects/vite-project/postcss.config.js create mode 100644 qiming-dev-inject/test-projects/vite-project/src/App.css create mode 100644 qiming-dev-inject/test-projects/vite-project/src/App.jsx create mode 100644 qiming-dev-inject/test-projects/vite-project/src/index.css create mode 100644 qiming-dev-inject/test-projects/vite-project/src/main.jsx create mode 100644 qiming-dev-inject/test-projects/vite-project/vite.config.js create mode 100644 qiming-dev-inject/test/e2e/e2e.test.js create mode 100644 qiming-dev-inject/test/integration/cli.test.js create mode 100644 qiming-dev-inject/test/integration/fixtures/index.html create mode 100644 qiming-dev-inject/test/integration/fixtures/test0.html create mode 100644 qiming-dev-inject/test/integration/fixtures/test1.html create mode 100644 qiming-dev-inject/test/integration/fixtures/test2.html create mode 100644 qiming-dev-inject/test/integration/fixtures/test3.html create mode 100644 qiming-dev-inject/test/integration/fixtures/test4.html create mode 100644 qiming-dev-inject/test/integration/fixtures/test5.html create mode 100644 qiming-dev-inject/test/integration/fixtures/test6.html create mode 100644 qiming-dev-inject/test/integration/fixtures/test7.html create mode 100644 qiming-dev-inject/test/integration/fixtures/test8.html create mode 100644 qiming-dev-inject/test/integration/fixtures/test9.html create mode 100644 qiming-dev-inject/test/integration/smartInject.test.js create mode 100644 qiming-dev-inject/test/package.json create mode 100644 qiming-dev-inject/test/test-runner.js create mode 100644 qiming-dev-inject/test/unit/detectProjectType.test.js create mode 100644 qiming-dev-inject/test/unit/fixtures/app/layout.js create mode 100644 qiming-dev-inject/test/unit/fixtures/index.html create mode 100644 qiming-dev-inject/test/unit/fixtures/pages/_document.js create mode 100644 qiming-dev-inject/test/unit/fixtures/test.html create mode 100644 qiming-dev-inject/test/unit/parseArgs.test.js create mode 100644 qiming-dev-inject/test/unit/utils.test.js diff --git a/.gitignore b/.gitignore index 07bf2802..d9d537af 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ !/qiming-file-server/ !/qiming-mobile/ !/qiming-claude-code-acp-ts/ +!/qiming-dev-inject/ !/qimingclaw/ !/qimingcode/ diff --git a/qiming-dev-inject/.gitignore b/qiming-dev-inject/.gitignore new file mode 100644 index 00000000..fde99533 --- /dev/null +++ b/qiming-dev-inject/.gitignore @@ -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 diff --git a/qiming-dev-inject/.npmignore b/qiming-dev-inject/.npmignore new file mode 100644 index 00000000..46266b57 --- /dev/null +++ b/qiming-dev-inject/.npmignore @@ -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 等会自动包含 diff --git a/qiming-dev-inject/EXAMPLES.md b/qiming-dev-inject/EXAMPLES.md new file mode 100644 index 00000000..2aa27ddb --- /dev/null +++ b/qiming-dev-inject/EXAMPLES.md @@ -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 +``` diff --git a/qiming-dev-inject/LICENSE b/qiming-dev-inject/LICENSE new file mode 100644 index 00000000..35137f43 --- /dev/null +++ b/qiming-dev-inject/LICENSE @@ -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. + diff --git a/qiming-dev-inject/README.md b/qiming-dev-inject/README.md new file mode 100644 index 00000000..b8542e1f --- /dev/null +++ b/qiming-dev-inject/README.md @@ -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. **脚本注入**:在 `` 标签前注入脚本 +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 diff --git a/qiming-dev-inject/bin/index.js b/qiming-dev-inject/bin/index.js new file mode 100644 index 00000000..96eaeb7c --- /dev/null +++ b/qiming-dev-inject/bin/index.js @@ -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(); diff --git a/qiming-dev-inject/lib/args.js b/qiming-dev-inject/lib/args.js new file mode 100644 index 00000000..e71b2b07 --- /dev/null +++ b/qiming-dev-inject/lib/args.js @@ -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; +} diff --git a/qiming-dev-inject/lib/code-cleaner.js b/qiming-dev-inject/lib/code-cleaner.js new file mode 100644 index 00000000..673ed8fa --- /dev/null +++ b/qiming-dev-inject/lib/code-cleaner.js @@ -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*`, 'g'); + return content.replace(regex, ''); + } + + /** + * 清理标签中间的空白行 + * @param {string} content - 要清理的内容 + * @param {string} tagName - 标签名(可以是正则表达式) + * @returns {string} 清理后的内容 + */ + static cleanTagWhitespace(content, tagName) { + // 清理如 \n\n\n -> \n + content = content.replace( + new RegExp(`(<${tagName}[^>]*>)\\s*\\n\\s*\\n(\\s*)`, '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; + } +} diff --git a/qiming-dev-inject/lib/file-finder.js b/qiming-dev-inject/lib/file-finder.js new file mode 100644 index 00000000..f87bc5f1 --- /dev/null +++ b/qiming-dev-inject/lib/file-finder.js @@ -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); +} + diff --git a/qiming-dev-inject/lib/framework-inject.js b/qiming-dev-inject/lib/framework-inject.js new file mode 100644 index 00000000..9f7b1c9c --- /dev/null +++ b/qiming-dev-inject/lib/framework-inject.js @@ -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 = '// '; + const endMarker = '// '; + + 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 = '' } = {}) { + const safeRemote = JSON.stringify(remote); // 避免注入问题 + const pluginName = 'dev-inject'; + const scriptId = 'dev-inject-monitor'; + + const scriptInjection = ` + + `; + + return ` + // + { + name: '${pluginName}', + enforce: 'post', // 确保在 HTML 注入阶段最后执行 + transformIndexHtml(html) { + if (!html.includes('data-id="${scriptId}"')) { + return html.replace(${JSON.stringify(insertPosition)}, \`${scriptInjection}\\n${insertPosition}\`); + } + return html; + } + }, + // + `; +} + + +// 为 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' && ( + `; + } else if (type === 'absolute-path') { + return ``; + } else { + throw new Error(`未知的远程类型: ${type}`); + } +} + +/** + * 从 HTML 内容中移除注入的脚本 - 同时清理前后的空白行 + */ +export function removeInjectedScripts(htmlContent, identifier = 'dev-inject') { + // 使用标准的开始/结束标识移除注入的代码 + // 匹配包括前后换行符和空白在内的完整块 + // 使用非贪婪匹配确保只匹配一个注入块 + const injectBlockRegex = /(\r?\n)?\s*[\s\S]*?\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 ''; + }); + + // 清理移除后可能产生的多余空白行 + // 清理 标签后的多余空白行(保留一个换行) + cleaned = cleaned.replace(/(]*>)\s*\n\s*\n+(\s*)/g, '$1\n$2'); + // 清理 前的多余空白行(保留一个换行) + 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 = ` + +`; + } else { + // 向后兼容:从 scriptTag 中提取 URL 并添加时间戳 + const scriptSrc = scriptTag.match(/src="([^"]*)"/)?.[1] || ''; + if (scriptSrc) { + // 提取基础 URL(移除可能已存在的时间戳参数) + const baseUrl = scriptSrc.split('?')[0]; + const separator = scriptSrc.includes('?') ? '&' : '?'; + scriptContent = ` + +`; + } else { + // 如果无法提取 URL,使用旧的静态方式 + scriptContent = ` + ${scriptTag} +`; + } + } + + // 查找 标签 + const headEndIndex = htmlContent.lastIndexOf(''); + + if (headEndIndex !== -1) { + // 在 之前注入 + const beforeHead = htmlContent.substring(0, headEndIndex); + const afterHead = htmlContent.substring(headEndIndex); + + // 检查 前是否有非空白内容 + 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}`; + } + } + + // 清理可能的多余空白行(保留最多一个空行) + // 清理 标签后的多余空白行 + result = result.replace(/(]*>)\s*\n\s*\n+(\s*)/g, '$1\n$2'); + // 清理 前的多余空白行 + 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 { + // 如果没有找到 ,查找 标签 + const bodyStartIndex = 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}`); +} diff --git a/qiming-dev-inject/package.json b/qiming-dev-inject/package.json new file mode 100644 index 00000000..02cf4dd2 --- /dev/null +++ b/qiming-dev-inject/package.json @@ -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" +} diff --git a/qiming-dev-inject/scripts/dev-monitor.js b/qiming-dev-inject/scripts/dev-monitor.js new file mode 100644 index 00000000..c59388a5 --- /dev/null +++ b/qiming-dev-inject/scripts/dev-monitor.js @@ -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 = ` +
+ DevMonitor + +
+
+ Errors (${monitorData.errors.length}): +
+ ${ + errors.length + ? errors + .map( + e => ` +
+ ${e.message} + ${e.details ? `
${e.details}` : ''} +
+ ` + ) + .join('') + : '
No errors
' + } +
+ + +
+ `; + + 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(); +})(); diff --git a/qiming-dev-inject/scripts/test-projects.js b/qiming-dev-inject/scripts/test-projects.js new file mode 100644 index 00000000..24944642 --- /dev/null +++ b/qiming-dev-inject/scripts/test-projects.js @@ -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('// ')) { + 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); +} + diff --git a/qiming-dev-inject/test-projects/README.md b/qiming-dev-inject/test-projects/README.md new file mode 100644 index 00000000..71690d02 --- /dev/null +++ b/qiming-dev-inject/test-projects/README.md @@ -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. 检查注入的脚本是否正确加载 + +## 贡献 + +欢迎添加更多测试项目或改进现有项目! + diff --git a/qiming-dev-inject/test-projects/html-project/.gitignore b/qiming-dev-inject/test-projects/html-project/.gitignore new file mode 100644 index 00000000..d3074e17 --- /dev/null +++ b/qiming-dev-inject/test-projects/html-project/.gitignore @@ -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* + diff --git a/qiming-dev-inject/test-projects/html-project/README.md b/qiming-dev-inject/test-projects/html-project/README.md new file mode 100644 index 00000000..5b6d99e8 --- /dev/null +++ b/qiming-dev-inject/test-projects/html-project/README.md @@ -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__` - 检查注入状态 + +## 功能特性 + +- 触发同步错误 +- 触发异步错误 +- 触发引用错误 +- 触发类型错误 +- 实时统计错误数据 +- 清除错误记录 + diff --git a/qiming-dev-inject/test-projects/html-project/index.html b/qiming-dev-inject/test-projects/html-project/index.html new file mode 100644 index 00000000..8378fcba --- /dev/null +++ b/qiming-dev-inject/test-projects/html-project/index.html @@ -0,0 +1,31 @@ + + + + + + + HTML 静态项目 - dev-inject 测试 + + + + + + +

HTML 静态项目测试

+

测试 dev-inject 在纯 HTML 项目中的注入功能

+ + + diff --git a/qiming-dev-inject/test-projects/html-project/package.json b/qiming-dev-inject/test-projects/html-project/package.json new file mode 100644 index 00000000..bc5efacb --- /dev/null +++ b/qiming-dev-inject/test-projects/html-project/package.json @@ -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" +} \ No newline at end of file diff --git a/qiming-dev-inject/test-projects/next-app-project/.gitignore b/qiming-dev-inject/test-projects/next-app-project/.gitignore new file mode 100644 index 00000000..1403e903 --- /dev/null +++ b/qiming-dev-inject/test-projects/next-app-project/.gitignore @@ -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 + diff --git a/qiming-dev-inject/test-projects/next-app-project/README.md b/qiming-dev-inject/test-projects/next-app-project/README.md new file mode 100644 index 00000000..20e904bf --- /dev/null +++ b/qiming-dev-inject/test-projects/next-app-project/README.md @@ -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__` - 检查注入状态 + diff --git a/qiming-dev-inject/test-projects/next-app-project/app/globals.css b/qiming-dev-inject/test-projects/next-app-project/app/globals.css new file mode 100644 index 00000000..b5c61c95 --- /dev/null +++ b/qiming-dev-inject/test-projects/next-app-project/app/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/qiming-dev-inject/test-projects/next-app-project/app/layout.tsx b/qiming-dev-inject/test-projects/next-app-project/app/layout.tsx new file mode 100644 index 00000000..5ef05df1 --- /dev/null +++ b/qiming-dev-inject/test-projects/next-app-project/app/layout.tsx @@ -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 ( + + + {/* DEV-INJECT-START */} + {process.env.NODE_ENV === 'development' && ( + + + + \ No newline at end of file diff --git a/qiming-dev-inject/test-projects/vite-project/package.json b/qiming-dev-inject/test-projects/vite-project/package.json new file mode 100644 index 00000000..d6eac0a1 --- /dev/null +++ b/qiming-dev-inject/test-projects/vite-project/package.json @@ -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" + } +} diff --git a/qiming-dev-inject/test-projects/vite-project/postcss.config.js b/qiming-dev-inject/test-projects/vite-project/postcss.config.js new file mode 100644 index 00000000..1054f2c2 --- /dev/null +++ b/qiming-dev-inject/test-projects/vite-project/postcss.config.js @@ -0,0 +1,12 @@ +// PostCSS 配置文件 +// 此文件用于防止 Vite 向上查找父目录的 PostCSS 配置 +// 本项目使用普通 CSS,不需要 PostCSS 插件 + +export default { + plugins: { + // 如果需要 PostCSS 插件,可以在这里添加 + // 例如:autoprefixer, tailwindcss 等 + } +} + + diff --git a/qiming-dev-inject/test-projects/vite-project/src/App.css b/qiming-dev-inject/test-projects/vite-project/src/App.css new file mode 100644 index 00000000..62db33e4 --- /dev/null +++ b/qiming-dev-inject/test-projects/vite-project/src/App.css @@ -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; +} diff --git a/qiming-dev-inject/test-projects/vite-project/src/App.jsx b/qiming-dev-inject/test-projects/vite-project/src/App.jsx new file mode 100644 index 00000000..b7e02174 --- /dev/null +++ b/qiming-dev-inject/test-projects/vite-project/src/App.jsx @@ -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 ( +
+
+
+

🟣 Vite Test Project

+

测试 dev-inject 框架感知注入功能

+
+ +
+

🚀 测试功能

+
+ + + +
+
+ +
+

📊 错误记录

+ {errors.length === 0 ? ( +

暂无错误

+ ) : ( +
    + {errors.map((error, index) => ( +
  • {error}
  • + ))} +
+ )} +
+ +
+

🔧 DevMonitor 检查

+

在浏览器控制台中检查:

+
    +
  • DevMonitor.showPanel() - 显示监控面板
  • +
  • DevMonitor.getStats() - 获取统计信息
  • +
  • window.__DEV_INJECT__ - 检查注入状态
  • +
+
+
+
+ ); +} + +export default App; diff --git a/qiming-dev-inject/test-projects/vite-project/src/index.css b/qiming-dev-inject/test-projects/vite-project/src/index.css new file mode 100644 index 00000000..c9e6946a --- /dev/null +++ b/qiming-dev-inject/test-projects/vite-project/src/index.css @@ -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; + } +} \ No newline at end of file diff --git a/qiming-dev-inject/test-projects/vite-project/src/main.jsx b/qiming-dev-inject/test-projects/vite-project/src/main.jsx new file mode 100644 index 00000000..54b39dd1 --- /dev/null +++ b/qiming-dev-inject/test-projects/vite-project/src/main.jsx @@ -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( + + + , +) diff --git a/qiming-dev-inject/test-projects/vite-project/vite.config.js b/qiming-dev-inject/test-projects/vite-project/vite.config.js new file mode 100644 index 00000000..6dbef817 --- /dev/null +++ b/qiming-dev-inject/test-projects/vite-project/vite.config.js @@ -0,0 +1,40 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [ + + // + { + name: 'dev-inject', + enforce: 'post', // 确保在 HTML 注入阶段最后执行 + transformIndexHtml(html) { + if (!html.includes('data-id="dev-inject-monitor"')) { + return html.replace("", ` + + \n`); + } + return html; + } + }, + // + + react() + ], + server: { + port: 3001 + } +}) diff --git a/qiming-dev-inject/test/e2e/e2e.test.js b/qiming-dev-inject/test/e2e/e2e.test.js new file mode 100644 index 00000000..7a193a33 --- /dev/null +++ b/qiming-dev-inject/test/e2e/e2e.test.js @@ -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': ` + + + Vite Test + + +
+ +`, + 'src/main.js': `import './app.js'`, + 'src/app.js': `console.log('Vite app loaded'); +document.getElementById('app').innerHTML = '

Vite App

';` + } + }, + '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 ( + + {children} + + )}`, + 'app/page.tsx': `export default function Home() { + return

Next.js App

; +}`, + 'app/globals.css': 'body { font-family: Arial, sans-serif; }' + } + }, + 'html-static': { + name: 'test-html-project', + files: { + 'index.html': ` + + + + Static HTML + + +

Static HTML Project

+

This is a static HTML project.

+ +` + } + } + }; + + 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, /'); + } + 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 ( + + {children} + + ) +}`; + 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 ( + + {children} + + ) +}`; + 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 ( + + + + + + +
+ + + + ) +}`; + 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 ( + + + My App + + +
+ + + + ) +}`; + 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
App Layout
; }'); + writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() { return
Document
; }'); + + const result = smartInject({ + remote: '/scripts/monitor.js', + dryRun: true + }, testDir); + + assert.strictEqual(result, true); + }); + }); + + describe('错误处理', () => { + test('应该处理不支持的框架类型', () => { + writeFileSync(join(testDir, 'index.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); + }); + }); +}); diff --git a/qiming-dev-inject/test/package.json b/qiming-dev-inject/test/package.json new file mode 100644 index 00000000..b244b0ee --- /dev/null +++ b/qiming-dev-inject/test/package.json @@ -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" + } +} diff --git a/qiming-dev-inject/test/test-runner.js b/qiming-dev-inject/test/test-runner.js new file mode 100644 index 00000000..61814185 --- /dev/null +++ b/qiming-dev-inject/test/test-runner.js @@ -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(); diff --git a/qiming-dev-inject/test/unit/detectProjectType.test.js b/qiming-dev-inject/test/unit/detectProjectType.test.js new file mode 100644 index 00000000..1e8aa2b0 --- /dev/null +++ b/qiming-dev-inject/test/unit/detectProjectType.test.js @@ -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'), ''); + 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'), ''); + const type = detectProjectType(testDir); + assert.strictEqual(type, 'create-react-app'); + }); + }); + + describe('HTML 静态项目检测', () => { + test('应该检测到 HTML 静态项目', () => { + writeFileSync(join(testDir, 'index.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'), ''); + 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'); + }); + }); +}); diff --git a/qiming-dev-inject/test/unit/fixtures/app/layout.js b/qiming-dev-inject/test/unit/fixtures/app/layout.js new file mode 100644 index 00000000..9d9d0186 --- /dev/null +++ b/qiming-dev-inject/test/unit/fixtures/app/layout.js @@ -0,0 +1 @@ +export default function Layout() {} \ No newline at end of file diff --git a/qiming-dev-inject/test/unit/fixtures/index.html b/qiming-dev-inject/test/unit/fixtures/index.html new file mode 100644 index 00000000..6c70bcfe --- /dev/null +++ b/qiming-dev-inject/test/unit/fixtures/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qiming-dev-inject/test/unit/fixtures/pages/_document.js b/qiming-dev-inject/test/unit/fixtures/pages/_document.js new file mode 100644 index 00000000..8b7222cd --- /dev/null +++ b/qiming-dev-inject/test/unit/fixtures/pages/_document.js @@ -0,0 +1 @@ +export default function Document() {} \ No newline at end of file diff --git a/qiming-dev-inject/test/unit/fixtures/test.html b/qiming-dev-inject/test/unit/fixtures/test.html new file mode 100644 index 00000000..6c70bcfe --- /dev/null +++ b/qiming-dev-inject/test/unit/fixtures/test.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qiming-dev-inject/test/unit/parseArgs.test.js b/qiming-dev-inject/test/unit/parseArgs.test.js new file mode 100644 index 00000000..a0399922 --- /dev/null +++ b/qiming-dev-inject/test/unit/parseArgs.test.js @@ -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); + }); + }); +}); diff --git a/qiming-dev-inject/test/unit/utils.test.js b/qiming-dev-inject/test/unit/utils.test.js new file mode 100644 index 00000000..0e0f768d --- /dev/null +++ b/qiming-dev-inject/test/unit/utils.test.js @@ -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, ''); + }); + + test('应该生成绝对路径类型的脚本标签', () => { + const tag = generateScriptTag('/scripts/monitor.js', 'absolute-path'); + assert.strictEqual(tag, ''); + }); + + 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, /^'; + const result = injectScriptToHtml(html, scriptTag); + + assert.match(result, /'; + const result = injectScriptToHtml(html, scriptTag); + + assert.match(result, //); + }); + + test('应该处理没有 标签的情况', () => { + const html = `Testcontent`; + const scriptTag = ''; + const result = injectScriptToHtml(html, scriptTag); + + assert.match(result, /'; + const result = injectScriptToHtml(html, scriptTag); + + assert.match(result, /'; + const result = injectScriptToHtml(html, scriptTag); + + assert.match(result, / + + +

Hello World

+ +`; + + const result = removeInjectedScripts(htmlWithInjection); + + assert.match(result, /\s*\s*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 + +`; + + const result = removeInjectedScripts(htmlWithMultiple); + + assert(!result.includes('script1.js')); + assert(!result.includes('script2.js')); + assert(!result.includes('injected by dev-inject')); + }); + + test('应该保留非注入的脚本', () => { + const htmlWithMixed = ` + + + Test +`; + + 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 = `Testcontent`; + const result = removeInjectedScripts(normalHtml); + + assert.strictEqual(result, normalHtml); + }); + }); + + describe('边界情况和错误处理', () => { + test('应该处理特殊字符', () => { + const html = 'Test & "Special" Characters'; + const scriptTag = ''; + const result = injectScriptToHtml(html, scriptTag); + + assert(result.includes('script.js')); + }); + + test('应该处理非常长的 HTML', () => { + const longHtml = '' + ''.repeat(1000) + ''; + const scriptTag = ''; + const result = injectScriptToHtml(longHtml, scriptTag); + + assert(result.length > longHtml.length); + assert(result.includes('test.js')); + }); + + test('应该处理格式不规范的 HTML', () => { + const malformedHtml = 'Testcontent'; + const scriptTag = ''; + const result = injectScriptToHtml(malformedHtml, scriptTag); + + assert(result.includes('test.js')); + }); + }); +});