This commit is contained in:
139
src/file-watcher.ts
Normal file
139
src/file-watcher.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { glob } from 'glob';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
|
||||
export interface FileWatcherOptions {
|
||||
patterns: string[];
|
||||
ignored?: string[];
|
||||
debounceMs?: number;
|
||||
}
|
||||
|
||||
export type FileChangeCallback = (event: 'add' | 'change' | 'unlink', filePath: string) => void;
|
||||
|
||||
export class FileWatcher {
|
||||
private chokidar: typeof import('chokidar') | null = null;
|
||||
private watcher: FSWatcher | null = null;
|
||||
private options: Required<FileWatcherOptions>;
|
||||
private callback: FileChangeCallback | null = null;
|
||||
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
|
||||
|
||||
constructor(options: FileWatcherOptions) {
|
||||
this.options = {
|
||||
patterns: options.patterns,
|
||||
ignored: options.ignored || ['node_modules/**', 'dist/**', '*.d.ts'],
|
||||
debounceMs: options.debounceMs || 300,
|
||||
};
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
const chokidarModule = await import('chokidar');
|
||||
this.chokidar = chokidarModule;
|
||||
}
|
||||
|
||||
start(callback: FileChangeCallback): void {
|
||||
if (!this.chokidar) {
|
||||
throw new Error('FileWatcher not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
this.callback = callback;
|
||||
|
||||
this.watcher = this.chokidar.watch(this.options.patterns, {
|
||||
ignored: this.options.ignored,
|
||||
persistent: true,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: this.options.debounceMs,
|
||||
pollInterval: 100,
|
||||
},
|
||||
});
|
||||
|
||||
this.watcher.on('add', (filePath: string) => this.handleEvent('add', filePath));
|
||||
this.watcher.on('change', (filePath: string) => this.handleEvent('change', filePath));
|
||||
this.watcher.on('unlink', (filePath: string) => this.handleEvent('unlink', filePath));
|
||||
}
|
||||
|
||||
private handleEvent(event: 'add' | 'change' | 'unlink', filePath: string): void {
|
||||
if (!this.callback) return;
|
||||
|
||||
const timerKey = filePath;
|
||||
|
||||
if (this.debounceTimers.has(timerKey)) {
|
||||
clearTimeout(this.debounceTimers.get(timerKey)!);
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.debounceTimers.delete(timerKey);
|
||||
this.callback!(event, filePath);
|
||||
}, this.options.debounceMs);
|
||||
|
||||
this.debounceTimers.set(timerKey, timer);
|
||||
}
|
||||
|
||||
async findFiles(patterns: string[]): Promise<string[]> {
|
||||
const allFiles: string[] = [];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const files = await glob(pattern, {
|
||||
ignore: this.options.ignored,
|
||||
});
|
||||
allFiles.push(...files);
|
||||
}
|
||||
|
||||
return [...new Set(allFiles)];
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.watcher) {
|
||||
this.watcher.close();
|
||||
this.watcher = null;
|
||||
}
|
||||
|
||||
for (const timer of this.debounceTimers.values()) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
this.debounceTimers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export async function readJsonFile(filePath: string): Promise<unknown> {
|
||||
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
export async function writeDeclarationFile(
|
||||
filePath: string,
|
||||
content: string
|
||||
): Promise<void> {
|
||||
const dir = path.dirname(filePath);
|
||||
await fs.promises.mkdir(dir, { recursive: true });
|
||||
await fs.promises.writeFile(filePath, content, 'utf-8');
|
||||
}
|
||||
|
||||
export function getDeclarationPath(jsonPath: string, outputDir?: string): string {
|
||||
const baseName = path.basename(jsonPath, path.extname(jsonPath));
|
||||
const dir = outputDir || path.dirname(jsonPath);
|
||||
return path.join(dir, `${baseName}.d.ts`);
|
||||
}
|
||||
|
||||
export function getInterfaceName(filePath: string): string {
|
||||
const baseName = path.basename(filePath, path.extname(filePath));
|
||||
return toPascalCase(baseName);
|
||||
}
|
||||
|
||||
export function toPascalCase(str: string): string {
|
||||
return str
|
||||
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
||||
.replace(/^(.)/, (c) => c.toUpperCase())
|
||||
.replace(/[A-Z]/g, (c, i) => (i === 0 ? c : c));
|
||||
}
|
||||
|
||||
export function extractJsonSamplesFromFile(filePath: string): unknown[] {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return [data];
|
||||
}
|
||||
Reference in New Issue
Block a user