chrome-extension-test-framework
Version:
Universal testing framework for Chrome extensions - Fast static analysis without browser dependencies
273 lines (234 loc) • 8.46 kB
JavaScript
/**
* FileWatcher - ファイル変更監視とスマートな再実行
*/
const fs = require('fs');
const path = require('path');
const EventEmitter = require('events');
class FileWatcher extends EventEmitter {
constructor(options = {}) {
super();
this.extensionPath = options.extensionPath;
this.debounceTime = options.debounceTime || 300; // ミリ秒
this.ignorePatterns = options.ignorePatterns || [
'node_modules',
'.git',
'test-results',
'test-output',
'.DS_Store',
'Thumbs.db',
'*.log',
'*.tmp',
'*.swp'
];
this.watchers = new Map();
this.changeQueue = new Map();
this.debounceTimer = null;
this.stats = {
totalChanges: 0,
fileTypes: {},
changedFiles: new Set()
};
}
/**
* 監視を開始
*/
start() {
console.log(`\n👀 Watching for changes in: ${this.extensionPath}`);
console.log(` Debounce: ${this.debounceTime}ms`);
console.log(` Ignoring: ${this.ignorePatterns.join(', ')}\n`);
// メインディレクトリを監視
this.watchDirectory(this.extensionPath);
// サブディレクトリを監視
const subdirs = ['js', 'css', '_locales', 'images', 'icons', 'html'];
subdirs.forEach(subdir => {
const fullPath = path.join(this.extensionPath, subdir);
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
this.watchDirectory(fullPath);
}
});
// manifest.jsonを特別に監視
const manifestPath = path.join(this.extensionPath, 'manifest.json');
if (fs.existsSync(manifestPath)) {
this.watchFile(manifestPath, 'manifest');
}
this.emit('start');
}
/**
* ディレクトリを監視
*/
watchDirectory(dirPath) {
try {
const watcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
if (filename && this.shouldWatch(filename)) {
this.handleChange(eventType, path.join(dirPath, filename));
}
});
this.watchers.set(dirPath, watcher);
console.log(` 📁 Watching directory: ${path.relative(this.extensionPath, dirPath)}`);
} catch (error) {
console.error(` ⚠️ Failed to watch directory: ${dirPath}`);
console.error(` ${error.message}`);
}
}
/**
* 特定のファイルを監視
*/
watchFile(filePath, type = 'file') {
try {
const watcher = fs.watch(filePath, (eventType) => {
this.handleChange(eventType, filePath, type);
});
this.watchers.set(filePath, watcher);
console.log(` 📄 Watching ${type}: ${path.relative(this.extensionPath, filePath)}`);
} catch (error) {
console.error(` ⚠️ Failed to watch file: ${filePath}`);
console.error(` ${error.message}`);
}
}
/**
* ファイルが監視対象かどうかを判定
*/
shouldWatch(filename) {
// 無視パターンに一致するかチェック
for (const pattern of this.ignorePatterns) {
if (pattern.includes('*')) {
// ワイルドカードパターン
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
if (regex.test(filename)) {
return false;
}
} else {
// 文字列マッチ
if (filename.includes(pattern)) {
return false;
}
}
}
// 拡張機能に関連するファイルのみ監視
const relevantExtensions = [
'.js', '.json', '.html', '.css', '.png', '.jpg', '.svg', '.xml'
];
const ext = path.extname(filename).toLowerCase();
return relevantExtensions.includes(ext) || filename === 'manifest.json';
}
/**
* ファイル変更を処理
*/
handleChange(eventType, filePath, fileType = null) {
const relativePath = path.relative(this.extensionPath, filePath);
// 変更をキューに追加
this.changeQueue.set(filePath, {
eventType,
relativePath,
fileType: fileType || this.getFileType(filePath),
timestamp: Date.now()
});
// デバウンス処理
this.debounce();
}
/**
* ファイルタイプを取得
*/
getFileType(filePath) {
const basename = path.basename(filePath);
const ext = path.extname(filePath).toLowerCase();
if (basename === 'manifest.json') return 'manifest';
if (basename === 'background.js' || basename === 'service-worker.js') return 'background';
if (basename.includes('content')) return 'content-script';
if (basename.includes('popup')) return 'popup';
if (ext === '.css') return 'stylesheet';
if (ext === '.html') return 'html';
if (['.png', '.jpg', '.svg', '.ico'].includes(ext)) return 'image';
if (ext === '.json') return 'json';
if (ext === '.js') return 'javascript';
return 'other';
}
/**
* デバウンス処理
*/
debounce() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.processChanges();
}, this.debounceTime);
}
/**
* 変更を処理
*/
processChanges() {
if (this.changeQueue.size === 0) return;
// 変更の統計を収集
const changes = Array.from(this.changeQueue.values());
const changedFiles = changes.map(c => c.relativePath);
const fileTypes = {};
changes.forEach(change => {
fileTypes[change.fileType] = (fileTypes[change.fileType] || 0) + 1;
this.stats.changedFiles.add(change.relativePath);
});
this.stats.totalChanges += changes.length;
Object.entries(fileTypes).forEach(([type, count]) => {
this.stats.fileTypes[type] = (this.stats.fileTypes[type] || 0) + count;
});
// 変更イベントを発行
const changeInfo = {
files: changedFiles,
fileTypes,
count: changes.length,
stats: this.stats
};
console.log(`\n📝 Detected ${changes.length} file change${changes.length > 1 ? 's' : ''}:`);
changedFiles.forEach(file => {
console.log(` - ${file}`);
});
this.emit('change', changeInfo);
// 特定のファイルタイプに応じた特別なイベント
if (fileTypes.manifest) {
this.emit('manifest-change', changeInfo);
}
if (fileTypes.background) {
this.emit('background-change', changeInfo);
}
if (fileTypes['content-script']) {
this.emit('content-script-change', changeInfo);
}
// キューをクリア
this.changeQueue.clear();
}
/**
* 監視を停止
*/
stop() {
for (const [path, watcher] of this.watchers) {
watcher.close();
}
this.watchers.clear();
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
console.log('\n🛑 File watching stopped');
this.emit('stop');
}
/**
* 統計情報を取得
*/
getStats() {
return {
...this.stats,
watchedPaths: Array.from(this.watchers.keys()),
isWatching: this.watchers.size > 0
};
}
/**
* 統計をリセット
*/
resetStats() {
this.stats = {
totalChanges: 0,
fileTypes: {},
changedFiles: new Set()
};
}
}
module.exports = FileWatcher;