2024-01-28 01:18:39 +01:00
|
|
|
import * as plugins from './smartchok.plugins.js';
|
|
|
|
import { Stringmap } from '@push.rocks/lik';
|
2017-06-30 18:05:55 +02:00
|
|
|
|
2018-03-01 00:08:08 +01:00
|
|
|
export type TSmartchokStatus = 'idle' | 'starting' | 'watching';
|
|
|
|
export type TFsEvent =
|
|
|
|
| 'add'
|
|
|
|
| 'addDir'
|
|
|
|
| 'change'
|
|
|
|
| 'error'
|
|
|
|
| 'unlink'
|
|
|
|
| 'unlinkDir'
|
|
|
|
| 'ready'
|
|
|
|
| 'raw';
|
2017-06-30 18:05:55 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Smartchok allows easy wathcing of files
|
|
|
|
*/
|
|
|
|
export class Smartchok {
|
2019-09-29 16:35:12 +02:00
|
|
|
public watchStringmap = new Stringmap();
|
|
|
|
public status: TSmartchokStatus = 'idle';
|
2025-06-26 23:15:42 +00:00
|
|
|
private watcher: plugins.chokidar.FSWatcher;
|
|
|
|
private globPatterns: string[] = [];
|
|
|
|
private globMatchers: Map<string, (path: string) => boolean> = new Map();
|
2018-10-10 17:06:40 +02:00
|
|
|
private watchingDeferred = plugins.smartpromise.defer<void>(); // used to run things when watcher is initialized
|
2018-03-01 00:08:08 +01:00
|
|
|
private eventObservablemap = new plugins.smartrx.Observablemap(); // register one observable per event
|
2017-06-30 18:05:55 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* constructor of class smartchok
|
|
|
|
*/
|
2024-01-28 01:18:39 +01:00
|
|
|
constructor(watchArrayArg: string[]) {
|
2018-03-01 00:08:08 +01:00
|
|
|
this.watchStringmap.addStringArray(watchArrayArg);
|
2024-01-28 01:18:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private getGlobBase(globPattern: string) {
|
|
|
|
// Characters that mark the beginning of a glob pattern
|
|
|
|
const globChars = ['*', '?', '[', ']', '{', '}'];
|
|
|
|
|
|
|
|
// Find the index of the first glob character
|
|
|
|
const firstGlobCharIndex = globPattern.split('').findIndex((char) => globChars.includes(char));
|
|
|
|
|
|
|
|
// If no glob characters are found, return the entire string
|
|
|
|
if (firstGlobCharIndex === -1) {
|
|
|
|
return globPattern;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extract the substring up to the first glob character
|
|
|
|
const basePathPortion = globPattern.substring(0, firstGlobCharIndex);
|
|
|
|
|
|
|
|
// Find the last slash before the glob pattern starts
|
|
|
|
const lastSlashIndex = basePathPortion.lastIndexOf('/');
|
|
|
|
|
|
|
|
// If there is no slash, return the basePathPortion as is
|
|
|
|
if (lastSlashIndex === -1) {
|
|
|
|
return basePathPortion;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the base path up to and including the last slash
|
|
|
|
return basePathPortion.substring(0, lastSlashIndex + 1);
|
2017-06-30 18:05:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* adds files to the list of watched files
|
|
|
|
*/
|
2019-09-29 16:35:12 +02:00
|
|
|
public add(pathArrayArg: string[]) {
|
2018-03-01 00:08:08 +01:00
|
|
|
this.watchStringmap.addStringArray(pathArrayArg);
|
2017-06-30 18:05:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* removes files from the list of watched files
|
|
|
|
*/
|
2019-09-29 16:35:12 +02:00
|
|
|
public remove(pathArg: string) {
|
2018-03-01 00:08:08 +01:00
|
|
|
this.watchStringmap.removeString(pathArg);
|
2017-06-30 18:05:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* gets an observable for a certain event
|
|
|
|
*/
|
2024-01-28 01:18:39 +01:00
|
|
|
public getObservableFor(
|
|
|
|
fsEvent: TFsEvent
|
|
|
|
): Promise<plugins.smartrx.rxjs.Observable<[string, plugins.fs.Stats]>> {
|
2019-09-29 16:35:12 +02:00
|
|
|
const done = plugins.smartpromise.defer<plugins.smartrx.rxjs.Observable<any>>();
|
2017-06-30 18:05:55 +02:00
|
|
|
this.watchingDeferred.promise.then(() => {
|
2024-01-28 01:18:39 +01:00
|
|
|
const eventObservable = this.eventObservablemap.getSubjectForEmitterEvent(
|
2018-03-01 00:08:08 +01:00
|
|
|
this.watcher,
|
|
|
|
fsEvent
|
|
|
|
);
|
|
|
|
done.resolve(eventObservable);
|
|
|
|
});
|
|
|
|
return done.promise;
|
2017-06-30 18:05:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* starts the watcher
|
|
|
|
* @returns Promise<void>
|
|
|
|
*/
|
2019-09-29 16:35:12 +02:00
|
|
|
public start(): Promise<void> {
|
|
|
|
const done = plugins.smartpromise.defer<void>();
|
2018-03-01 00:08:08 +01:00
|
|
|
this.status = 'starting';
|
2025-06-26 23:15:42 +00:00
|
|
|
|
|
|
|
// Store original glob patterns and create matchers
|
|
|
|
this.globPatterns = this.watchStringmap.getStringArray();
|
|
|
|
const basePaths = new Set<string>();
|
|
|
|
|
|
|
|
this.globPatterns.forEach((pattern) => {
|
|
|
|
const basePath = this.getGlobBase(pattern);
|
|
|
|
basePaths.add(basePath);
|
|
|
|
|
|
|
|
// Create a picomatch matcher for each glob pattern
|
|
|
|
const matcher = plugins.picomatch(pattern, {
|
|
|
|
dot: true,
|
|
|
|
basename: false
|
|
|
|
});
|
|
|
|
this.globMatchers.set(pattern, matcher);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Convert Set to Array for chokidar
|
|
|
|
const watchPaths = Array.from(basePaths);
|
|
|
|
console.log('Base paths to watch:', watchPaths);
|
|
|
|
|
|
|
|
this.watcher = plugins.chokidar.watch(watchPaths, {
|
|
|
|
persistent: true,
|
|
|
|
ignoreInitial: false,
|
|
|
|
followSymlinks: false,
|
|
|
|
depth: 4,
|
|
|
|
awaitWriteFinish: {
|
|
|
|
stabilityThreshold: 300,
|
|
|
|
pollInterval: 100
|
|
|
|
},
|
|
|
|
ignored: (path: string, stats?: plugins.fs.Stats) => {
|
|
|
|
// Don't filter during initialization - let chokidar watch everything
|
|
|
|
// We'll filter events as they come in
|
|
|
|
return false;
|
2024-01-28 17:25:47 +01:00
|
|
|
}
|
2025-06-26 23:15:42 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Set up event handlers with glob filtering
|
|
|
|
const fileEvents: Array<'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'> =
|
|
|
|
['add', 'addDir', 'change', 'unlink', 'unlinkDir'];
|
|
|
|
|
|
|
|
// Handle file events
|
|
|
|
fileEvents.forEach(eventName => {
|
|
|
|
this.watcher.on(eventName, (path: string, stats?: plugins.fs.Stats) => {
|
|
|
|
// Only emit event if the path matches our glob patterns
|
|
|
|
if (this.shouldWatchPath(path)) {
|
|
|
|
this.eventObservablemap.getSubjectForEmitterEvent(this.watcher, eventName)
|
|
|
|
.next([path, stats]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// Handle error events
|
|
|
|
this.watcher.on('error', (error: Error) => {
|
|
|
|
this.eventObservablemap.getSubjectForEmitterEvent(this.watcher, 'error')
|
|
|
|
.next([error, undefined]);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Handle raw events
|
|
|
|
this.watcher.on('raw', (eventType: string, path: string, details: any) => {
|
|
|
|
if (this.shouldWatchPath(path)) {
|
|
|
|
this.eventObservablemap.getSubjectForEmitterEvent(this.watcher, 'raw')
|
|
|
|
.next([path, undefined]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-06-30 18:05:55 +02:00
|
|
|
this.watcher.on('ready', () => {
|
2018-03-01 00:08:08 +01:00
|
|
|
this.status = 'watching';
|
|
|
|
this.watchingDeferred.resolve();
|
|
|
|
done.resolve();
|
|
|
|
});
|
|
|
|
return done.promise;
|
2017-06-30 18:05:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* stop the watcher process if watching
|
|
|
|
*/
|
2021-11-29 20:26:59 +01:00
|
|
|
public async stop() {
|
|
|
|
const closeWatcher = async () => {
|
|
|
|
await this.watcher.close();
|
2018-03-01 00:08:08 +01:00
|
|
|
};
|
2017-06-30 18:05:55 +02:00
|
|
|
if (this.status === 'watching') {
|
2018-03-01 00:08:08 +01:00
|
|
|
console.log('closing while watching');
|
2021-11-29 20:26:59 +01:00
|
|
|
await closeWatcher();
|
2017-06-30 18:05:55 +02:00
|
|
|
} else if (this.status === 'starting') {
|
2021-11-29 20:26:59 +01:00
|
|
|
await this.watchingDeferred.promise;
|
|
|
|
await closeWatcher();
|
2017-06-30 18:05:55 +02:00
|
|
|
}
|
|
|
|
}
|
2025-06-26 23:15:42 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if a path should be watched based on glob patterns
|
|
|
|
*/
|
|
|
|
private shouldWatchPath(filePath: string): boolean {
|
|
|
|
// Normalize the path - remove leading ./ if present
|
|
|
|
let normalizedPath = filePath.replace(/\\/g, '/');
|
|
|
|
if (normalizedPath.startsWith('./')) {
|
|
|
|
normalizedPath = normalizedPath.substring(2);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if the path matches any of our glob patterns
|
|
|
|
for (const [pattern, matcher] of this.globMatchers) {
|
|
|
|
// Also normalize the pattern for comparison
|
|
|
|
let normalizedPattern = pattern;
|
|
|
|
if (normalizedPattern.startsWith('./')) {
|
|
|
|
normalizedPattern = normalizedPattern.substring(2);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try matching with both the original pattern and normalized
|
|
|
|
if (matcher(normalizedPath) || matcher(filePath)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Also try matching without the leading path
|
|
|
|
const withoutLeading = 'test/' + normalizedPath.split('test/').slice(1).join('test/');
|
|
|
|
if (matcher(withoutLeading)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2017-06-30 18:05:55 +02:00
|
|
|
}
|