import assert from 'node:assert/strict'; import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { test } from 'node:test'; import { setTimeout as delay } from 'node:timers/promises'; import { fileURLToPath } from 'node:url'; import { createServer, loadConfigFromFile, normalizePath } from 'vite'; test( 'AGC 排除 Rust 构建目录且保留源码与共享组件监听', { timeout: 30_000 }, async () => { const loaded = await loadConfigFromFile( { command: 'serve', mode: 'development' }, fileURLToPath(new URL('../vite.config.ts', import.meta.url)), ); assert.ok(loaded); assert.notEqual(loaded.config.server?.watch, null); assert.notEqual(loaded.config.server?.hmr, false); assert.ok( [loaded.config.server?.watch?.ignored] .flat() .includes('**/src-tauri/target/**'), ); const fixture = await mkdtemp(join(tmpdir(), 'agc-vite-watch-')); const root = join(fixture, 'apps', 'ai-game-creator-shell'); const source = join(root, 'src', 'main.js'); const css = join(root, 'src', 'styles.css'); const shared = join(fixture, 'packages', 'shared', 'src', 'component.js'); const target = join(root, 'src-tauri', 'target'); const artifact = join(target, 'debug', 'incremental', 'cache.bin'); let server; try { for (const file of [source, css, shared, artifact]) { await mkdir(dirname(file), { recursive: true }); await writeFile( file, file === css ? 'body { color: red; }' : 'export default 1;', ); } // 使用真实 Vite watcher 和实际配置,仅将扫描根替换为小型夹具; // 不加载业务插件、后端或原生窗口,也不扫描开发机上的大型 target。 server = await createServer({ configFile: false, envFile: false, root, logLevel: 'silent', server: { watch: loaded.config.server?.watch, middlewareMode: true, hmr: false, fs: { allow: [fixture] }, }, optimizeDeps: { noDiscovery: true, include: [] }, }); const waitForWatchedFile = async (file) => { const normalized = normalizePath(file); for (let attempt = 0; attempt < 100; attempt += 1) { if ( Object.entries(server.watcher.getWatched()).some( ([directory, names]) => names.some( (name) => normalizePath(join(directory, name)) === normalized, ), ) ) return; await delay(50); } assert.fail(`源码必须仍被监听:${normalized}`); }; await waitForWatchedFile(source); // 真实模块转换应将 root 外的共享源码加入监听。 await server.transformRequest(`/@fs/${normalizePath(shared)}`); for (const file of [source, css, shared]) { const normalized = normalizePath(file); await waitForWatchedFile(file); const changed = new Promise((resolve, reject) => { const timer = setTimeout(() => { server.watcher.off('change', onChange); reject(new Error(`未收到源码变更:${normalized}`)); }, 5_000); function onChange(path) { if (normalizePath(path) !== normalized) return; clearTimeout(timer); server.watcher.off('change', onChange); resolve(); } server.watcher.on('change', onChange); }); await writeFile( file, file === css ? 'body { color: blue; }' : 'export default 2;', ); await changed; } const targetPath = normalizePath(target); const targetDirectories = Object.keys(server.watcher.getWatched()) .map(normalizePath) .filter( (path) => path === targetPath || path.startsWith(`${targetPath}/`), ); assert.deepEqual(targetDirectories, [], 'Rust target 不应创建目录监听器'); } finally { await server?.close(); await rm(fixture, { recursive: true, force: true }); } }, );