提交qiming-dev-inject

This commit is contained in:
Codex
2026-06-01 12:53:19 +08:00
parent 96af3b324b
commit 9e9486b7c2
76 changed files with 6397 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
import { test, describe } from 'node:test';
import assert from 'node:assert';
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 导入要测试的模块
import { detectProjectType } from '../../lib/framework-inject.js';
describe('框架检测功能', () => {
const testDir = join(__dirname, 'fixtures');
// 测试前设置
test.before(() => {
// 确保测试目录存在
if (!existsSync(testDir)) {
mkdirSync(testDir, { recursive: true });
}
});
// 测试后清理
test.after(() => {
// 清理测试文件
const filesToClean = [
'vite.config.js',
'vite.config.ts',
'next.config.js',
'app/layout.tsx',
'pages/_document.tsx',
'public/index.html',
'index.html'
];
filesToClean.forEach(file => {
const filePath = join(testDir, file);
if (existsSync(filePath)) {
rmSync(filePath);
}
});
});
describe('Vite 项目检测', () => {
test('应该检测到 Vite 项目 (js)', () => {
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'vite');
});
test('应该检测到 Vite 项目 (ts)', () => {
writeFileSync(join(testDir, 'vite.config.ts'), 'export default {};');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'vite');
});
test('优先检测 Vite 而不是其他配置', () => {
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
writeFileSync(join(testDir, 'public/index.html'), '<html></html>');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'vite');
});
});
describe('Next.js App Router 检测', () => {
test('应该检测到 Next.js App Router', () => {
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
mkdirSync(join(testDir, 'app'), { recursive: true });
writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() {}');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'next-app');
});
test('应该检测到 Next.js App Router (js)', () => {
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
mkdirSync(join(testDir, 'app'), { recursive: true });
writeFileSync(join(testDir, 'app', 'layout.js'), 'export default function Layout() {}');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'next-app');
});
});
describe('Next.js Pages Router 检测', () => {
test('应该检测到 Next.js Pages Router', () => {
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
mkdirSync(join(testDir, 'pages'), { recursive: true });
writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() {}');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'next-pages');
});
test('应该检测到 Next.js Pages Router (js)', () => {
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
mkdirSync(join(testDir, 'pages'), { recursive: true });
writeFileSync(join(testDir, 'pages', '_document.js'), 'export default function Document() {}');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'next-pages');
});
});
describe('Next.js 混合模式检测', () => {
test('应该检测到 Next.js 混合模式', () => {
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
mkdirSync(join(testDir, 'app'), { recursive: true });
mkdirSync(join(testDir, 'pages'), { recursive: true });
writeFileSync(join(testDir, 'app', 'layout.tsx'), 'export default function Layout() {}');
writeFileSync(join(testDir, 'pages', '_document.tsx'), 'export default function Document() {}');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'next-hybrid');
});
});
describe('Create React App 检测', () => {
test('应该检测到 Create React App', () => {
mkdirSync(join(testDir, 'public'), { recursive: true });
writeFileSync(join(testDir, 'public', 'index.html'), '<html></html>');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'create-react-app');
});
});
describe('HTML 静态项目检测', () => {
test('应该检测到 HTML 静态项目', () => {
writeFileSync(join(testDir, 'index.html'), '<html></html>');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'html-static');
});
});
describe('未知项目检测', () => {
test('应该返回 unknown 对于空目录', () => {
const type = detectProjectType(testDir);
assert.strictEqual(type, 'unknown');
});
test('应该返回 unknown 对于不存在的目录', () => {
const type = detectProjectType('/non-existent-directory');
assert.strictEqual(type, 'unknown');
});
});
describe('优先级检测', () => {
test('Next.js 优先于 HTML', () => {
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
writeFileSync(join(testDir, 'index.html'), '<html></html>');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'next-app'); // 默认是 app 模式
});
test('Vite 优先于 Next.js', () => {
writeFileSync(join(testDir, 'vite.config.js'), 'module.exports = {};');
writeFileSync(join(testDir, 'next.config.js'), 'module.exports = {};');
const type = detectProjectType(testDir);
assert.strictEqual(type, 'vite');
});
});
});

View File

@@ -0,0 +1 @@
export default function Layout() {}

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1 @@
export default function Document() {}

View File

@@ -0,0 +1 @@
<html></html>

View File

@@ -0,0 +1,161 @@
import { test, describe } from 'node:test';
import assert from 'node:assert';
// 导入要测试的模块
import { parseArgs } from '../../lib/args.js';
describe('CLI 参数解析', () => {
describe('基本命令解析', () => {
test('应该解析 install 命令', () => {
const result = parseArgs(['install', '--remote', '/scripts/test.js']);
assert.strictEqual(result.command, 'install');
assert.strictEqual(result.options.remote, '/scripts/test.js');
});
test('应该解析 uninstall 命令', () => {
const result = parseArgs(['uninstall']);
assert.strictEqual(result.command, 'uninstall');
});
test('应该处理空参数', () => {
const result = parseArgs([]);
assert.strictEqual(result.command, null);
assert.deepStrictEqual(result.options, {});
});
});
describe('--remote 参数解析', () => {
test('应该解析 --remote=value 格式', () => {
const result = parseArgs(['install', '--remote=http://localhost:9000/monitor.js']);
assert.strictEqual(result.options.remote, 'http://localhost:9000/monitor.js');
});
test('应该解析 --remote value 格式', () => {
const result = parseArgs(['install', '--remote', '/scripts/dev-monitor.js']);
assert.strictEqual(result.options.remote, '/scripts/dev-monitor.js');
});
test('应该处理多个 --remote 参数(使用最后一个)', () => {
const result = parseArgs(['install', '--remote', '/first.js', '--remote', '/second.js']);
assert.strictEqual(result.options.remote, '/second.js');
});
});
describe('--file 参数解析', () => {
test('应该解析 --file=value 格式', () => {
const result = parseArgs(['install', '--remote=/test.js', '--file=public/index.html']);
assert.strictEqual(result.options.file, 'public/index.html');
});
test('应该解析 --file value 格式', () => {
const result = parseArgs(['install', '--remote=/test.js', '--file', './public/index.html']);
assert.strictEqual(result.options.file, './public/index.html');
});
});
describe('--framework 参数解析', () => {
test('应该解析 --framework 选项', () => {
const result = parseArgs(['install', '--remote=/test.js', '--framework']);
assert.strictEqual(result.options.framework, true);
});
test('应该解析 -f 简写选项', () => {
const result = parseArgs(['install', '--remote=/test.js', '-f']);
assert.strictEqual(result.options.framework, true);
});
});
describe('--dry-run 参数解析', () => {
test('应该解析 --dry-run 选项', () => {
const result = parseArgs(['install', '--remote=/test.js', '--dry-run']);
assert.strictEqual(result.options.dryRun, true);
});
});
describe('--verbose 参数解析', () => {
test('应该解析 --verbose 选项', () => {
const result = parseArgs(['install', '--remote=/test.js', '--verbose']);
assert.strictEqual(result.options.verbose, true);
});
});
describe('复合选项解析', () => {
test('应该解析多个选项', () => {
const result = parseArgs([
'install',
'--remote', 'http://localhost:9000/monitor.js',
'--framework',
'--dry-run',
'--verbose',
'--file', './test.html'
]);
assert.strictEqual(result.command, 'install');
assert.strictEqual(result.options.remote, 'http://localhost:9000/monitor.js');
assert.strictEqual(result.options.framework, true);
assert.strictEqual(result.options.dryRun, true);
assert.strictEqual(result.options.verbose, true);
assert.strictEqual(result.options.file, './test.html');
});
});
describe('错误处理', () => {
test('install 命令缺少 --remote 参数应该抛出错误', () => {
assert.throws(() => {
parseArgs(['install']);
}, /install 命令需要 --remote 参数/);
});
test('uninstall 命令不需要 --remote 参数', () => {
const result = parseArgs(['uninstall']);
assert.strictEqual(result.command, 'uninstall');
assert.deepStrictEqual(result.options, {});
});
});
describe('无效参数处理', () => {
test('应该忽略未知参数', () => {
// 使用 mock console.warn 来捕获警告
const originalWarn = console.warn;
let warnCalled = false;
console.warn = () => { warnCalled = true; };
const result = parseArgs(['install', '--remote=/test.js', '--unknown-option']);
console.warn = originalWarn;
// 应该仍然正确解析已知参数
assert.strictEqual(result.options.remote, '/test.js');
// 应该标记未知参数被忽略(通过 console.warn
assert.ok(warnCalled); // 如果有警告输出则测试通过
});
});
describe('参数顺序无关性', () => {
test('应该正确处理不同顺序的参数', () => {
const result1 = parseArgs(['install', '--remote=/test.js', '--framework', '--dry-run']);
const result2 = parseArgs(['install', '--dry-run', '--framework', '--remote=/test.js']);
assert.deepStrictEqual(result1.options, result2.options);
});
});
describe('边界情况', () => {
test('应该处理空的 --remote 值', () => {
const result = parseArgs(['install', '--remote', '']);
assert.strictEqual(result.options.remote, '');
});
test('应该处理包含特殊字符的路径', () => {
const complexPath = '/path with spaces/file.js';
const result = parseArgs(['install', '--remote', complexPath]);
assert.strictEqual(result.options.remote, complexPath);
});
test('应该处理长参数值', () => {
const longValue = 'a'.repeat(1000);
const result = parseArgs(['install', '--remote', longValue]);
assert.strictEqual(result.options.remote, longValue);
});
});
});

View File

@@ -0,0 +1,304 @@
import { test, describe } from 'node:test';
import assert from 'node:assert';
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 导入要测试的模块
import {
parseRemoteType,
generateScriptTag,
lookupFiles,
injectScriptToHtml,
removeInjectedScripts
} from '../../lib/utils.js';
describe('工具函数', () => {
const testDir = join(__dirname, 'fixtures');
test.before(() => {
if (!existsSync(testDir)) {
mkdirSync(testDir, { recursive: true });
}
});
test.after(() => {
const filesToClean = [
'test1.html',
'test2.html',
'subdir/test3.html',
'subdir/test4.html'
];
filesToClean.forEach(file => {
const filePath = join(testDir, file);
if (existsSync(filePath)) {
rmSync(filePath);
}
});
const subDir = join(testDir, 'subdir');
if (existsSync(subDir)) {
rmSync(subDir, { recursive: true });
}
});
describe('parseRemoteType', () => {
test('应该识别 HTTP URL', () => {
assert.strictEqual(parseRemoteType('http://localhost:9000/monitor.js'), 'url');
assert.strictEqual(parseRemoteType('http://example.com/script.js'), 'url');
});
test('应该识别 HTTPS URL', () => {
assert.strictEqual(parseRemoteType('https://cdn.example.com/monitor.js'), 'url');
assert.strictEqual(parseRemoteType('https://localhost:3000/dev-monitor.js'), 'url');
});
test('应该识别绝对路径', () => {
assert.strictEqual(parseRemoteType('/scripts/monitor.js'), 'absolute-path');
assert.strictEqual(parseRemoteType('/assets/dev-monitor.js'), 'absolute-path');
assert.strictEqual(parseRemoteType('/path/to/script.js'), 'absolute-path');
});
test('应该处理相对路径', () => {
try {
parseRemoteType('./script.js');
assert.fail('应该抛出错误');
} catch (error) {
assert.match(error.message, /不支持的远程路径格式/);
}
});
test('应该处理空字符串', () => {
try {
parseRemoteType('');
assert.fail('应该抛出错误');
} catch (error) {
assert.match(error.message, /不支持的远程路径格式/);
}
});
});
describe('generateScriptTag', () => {
test('应该生成 URL 类型的脚本标签', () => {
const tag = generateScriptTag('http://localhost:9000/monitor.js', 'url');
assert.strictEqual(tag, '<script src="http://localhost:9000/monitor.js"></script>');
});
test('应该生成绝对路径类型的脚本标签', () => {
const tag = generateScriptTag('/scripts/monitor.js', 'absolute-path');
assert.strictEqual(tag, '<script src="/scripts/monitor.js"></script>');
});
test('应该处理不同的路径格式', () => {
const testCases = [
'https://cdn.example.com/monitor.js',
'http://localhost:3000/dev-monitor.js',
'/assets/scripts/dev-monitor.js',
'/scripts/monitor.js'
];
testCases.forEach(path => {
const type = parseRemoteType(path);
const tag = generateScriptTag(path, type);
assert.match(tag, /^<script src="[^"]*"><\/script>$/);
assert(tag.includes(path));
});
});
});
describe('lookupFiles', () => {
test('应该查找指定目录中的 HTML 文件', () => {
// 创建测试文件
writeFileSync(join(testDir, 'test1.html'), '<html></html>');
writeFileSync(join(testDir, 'test2.html'), '<html></html>');
mkdirSync(join(testDir, 'subdir'), { recursive: true });
writeFileSync(join(testDir, 'subdir', 'test3.html'), '<html></html>');
const files = lookupFiles(testDir);
// 应该找到所有 HTML 文件
assert(files.length >= 3);
assert(files.some(f => f.includes('test1.html')));
assert(files.some(f => f.includes('test2.html')));
assert(files.some(f => f.includes('test3.html')));
});
test('应该优先返回 index.html', () => {
// 创建测试文件
writeFileSync(join(testDir, 'test.html'), '<html></html>');
writeFileSync(join(testDir, 'index.html'), '<html></html>');
const files = lookupFiles(testDir);
// 应该优先返回 index.html
assert.strictEqual(files.length, 1);
assert(files[0].includes('index.html'));
});
test('应该处理空目录', () => {
const files = lookupFiles(testDir);
assert.deepStrictEqual(files, []);
});
test('应该处理不存在的目录', () => {
const files = lookupFiles('/non-existent-directory');
assert.deepStrictEqual(files, []);
});
test('应该跳过 node_modules 目录', () => {
// 创建测试文件和 node_modules 目录
writeFileSync(join(testDir, 'test.html'), '<html></html>');
mkdirSync(join(testDir, 'node_modules'), { recursive: true });
writeFileSync(join(testDir, 'node_modules', 'index.html'), '<html></html>');
const files = lookupFiles(testDir);
// 不应该包含 node_modules 中的文件
assert(files.length === 1);
assert(files[0].includes('test.html'));
assert(!files.some(f => f.includes('node_modules')));
});
});
describe('injectScriptToHtml', () => {
test('应该注入脚本到 HTML 的 </head> 标签之前', () => {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>`;
const scriptTag = '<script src="/scripts/monitor.js"></script>';
const result = injectScriptToHtml(html, scriptTag);
assert.match(result, /<script src="\/scripts\/monitor\.js"><\/script>\s*<\/head>/);
assert.match(result, /dev-inject/);
});
test('应该添加 dev-inject 标识注释', () => {
const html = `<html><head></head><body></body></html>`;
const scriptTag = '<script src="/test.js"></script>';
const result = injectScriptToHtml(html, scriptTag);
assert.match(result, /<!-- injected by dev-inject -->/);
});
test('应该处理没有 </head> 标签的情况', () => {
const html = `<html><head><title>Test</title></head><body>content</body></html>`;
const scriptTag = '<script src="/test.js"></script>';
const result = injectScriptToHtml(html, scriptTag);
assert.match(result, /<script src="\/test\.js"><\/script>/);
});
test('应该处理没有 <head> 标签的情况', () => {
const html = `<html><body>content</body></html>`;
const scriptTag = '<script src="/test.js"></script>';
const result = injectScriptToHtml(html, scriptTag);
assert.match(result, /<script src="\/test\.js"><\/script>/);
});
test('应该处理空 HTML', () => {
const html = '';
const scriptTag = '<script src="/test.js"></script>';
const result = injectScriptToHtml(html, scriptTag);
assert.match(result, /<script src="\/test\.js"><\/script>/);
});
});
describe('removeInjectedScripts', () => {
test('应该移除注入的脚本和注释', () => {
const htmlWithInjection = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test</title>
<script src="/scripts/monitor.js"></script> <!-- injected by dev-inject -->
</head>
<body>
<h1>Hello World</h1>
</body>
</html>`;
const result = removeInjectedScripts(htmlWithInjection);
assert.match(result, /<head>\s*<meta charset="UTF-8">\s*<title>Test<\/title>\s*<\/head>/);
assert(!result.includes('dev-monitor.js'));
assert(!result.includes('injected by dev-inject'));
});
test('应该处理多个注入的脚本', () => {
const htmlWithMultiple = `<html><head>
<script src="/script1.js"></script> <!-- injected by dev-inject -->
<title>Test</title>
<script src="/script2.js"></script> <!-- injected by dev-inject -->
</head></html>`;
const result = removeInjectedScripts(htmlWithMultiple);
assert(!result.includes('script1.js'));
assert(!result.includes('script2.js'));
assert(!result.includes('injected by dev-inject'));
});
test('应该保留非注入的脚本', () => {
const htmlWithMixed = `<html><head>
<script src="/normal-script.js"></script>
<script src="/injected-script.js"></script> <!-- injected by dev-inject -->
<title>Test</title>
</head></html>`;
const result = removeInjectedScripts(htmlWithMixed);
assert(result.includes('normal-script.js'));
assert(!result.includes('injected-script.js'));
assert(!result.includes('injected by dev-inject'));
});
test('应该处理没有注入脚本的 HTML', () => {
const normalHtml = `<html><head><title>Test</title></head><body>content</body></html>`;
const result = removeInjectedScripts(normalHtml);
assert.strictEqual(result, normalHtml);
});
});
describe('边界情况和错误处理', () => {
test('应该处理特殊字符', () => {
const html = '<html><head><title>Test & "Special" Characters</title></head></html>';
const scriptTag = '<script src="/path/with spaces/script.js"></script>';
const result = injectScriptToHtml(html, scriptTag);
assert(result.includes('script.js'));
});
test('应该处理非常长的 HTML', () => {
const longHtml = '<html><head>' + '<meta charset="UTF-8">'.repeat(1000) + '</head></html>';
const scriptTag = '<script src="/test.js"></script>';
const result = injectScriptToHtml(longHtml, scriptTag);
assert(result.length > longHtml.length);
assert(result.includes('test.js'));
});
test('应该处理格式不规范的 HTML', () => {
const malformedHtml = '<html><HEAD><title>Test</Title></HEAD><BODY>content</BODY></HTML>';
const scriptTag = '<script src="/test.js"></script>';
const result = injectScriptToHtml(malformedHtml, scriptTag);
assert(result.includes('test.js'));
});
});
});