Initial upload with CI/CD workflow
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-31 12:25:58 +00:00
parent 8a36547ec3
commit ade2c3be47

139
src/file-watcher.ts Normal file
View 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];
}