-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(plugin-hooks): add watchChange hook
- Loading branch information
Showing
6 changed files
with
223 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import type { EventListener } from './types'; | ||
|
||
export class WatchEmitter<T extends Record<string, (...args: any[]) => any>> { | ||
private handlers: { | ||
[K in keyof T]?: EventListener<T, K>[]; | ||
} = Object.create(null); | ||
|
||
async close(): Promise<void> {} | ||
|
||
emit<K extends keyof T>( | ||
event: K, | ||
...args: Parameters<T[K]> | ||
): Promise<unknown> { | ||
const listeners = this.handlers[event] || []; | ||
return Promise.all( | ||
listeners.map(async (handler) => await handler(...args)), | ||
); | ||
} | ||
|
||
on<K extends keyof T>(event: K, listener: EventListener<T, K>): this { | ||
if (!this.handlers[event]) { | ||
this.handlers[event] = []; | ||
} | ||
this.handlers[event]!.push(listener); | ||
return this; | ||
} | ||
|
||
off<K extends keyof T>(event: K, listener: EventListener<T, K>): this { | ||
const listeners = this.handlers[event]; | ||
if (listeners) { | ||
const index = listeners.indexOf(listener); | ||
if (index !== -1) { | ||
listeners.splice(index, 1); | ||
} | ||
} | ||
return this; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import chokidar, { FSWatcher } from 'chokidar'; | ||
import type { ChangeEvent, ChokidarOptions } from './types'; | ||
|
||
export class FileWatcher { | ||
private watcher: FSWatcher; | ||
private transformWatchers = new Map<string, FSWatcher>(); | ||
|
||
constructor( | ||
private onChange: (id: string, event: ChangeEvent) => void, | ||
private options: ChokidarOptions = {}, | ||
) { | ||
this.watcher = this.createWatcher(); | ||
} | ||
|
||
watch(id: string): void { | ||
this.watcher.add(id); | ||
} | ||
|
||
unwatch(id: string): void { | ||
this.watcher.unwatch(id); | ||
const transformWatcher = this.transformWatchers.get(id); | ||
if (transformWatcher) { | ||
transformWatcher.close(); | ||
this.transformWatchers.delete(id); | ||
} | ||
} | ||
|
||
close(): void { | ||
this.watcher.close(); | ||
for (const watcher of this.transformWatchers.values()) { | ||
watcher.close(); | ||
} | ||
} | ||
|
||
private createWatcher(): FSWatcher { | ||
return chokidar | ||
.watch([], this.options) | ||
.on('add', (id) => this.onChange(id, 'create')) | ||
.on('change', (id) => this.onChange(id, 'update')) | ||
.on('unlink', (id) => this.onChange(id, 'delete')); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
export type ChangeEvent = 'create' | 'update' | 'delete'; | ||
|
||
export type ChokidarOptions = { | ||
ignored?: string | RegExp | Array<string | RegExp>; | ||
persistent?: boolean; | ||
ignoreInitial?: boolean; | ||
followSymlinks?: boolean; | ||
cwd?: string; | ||
disableGlobbing?: boolean; | ||
usePolling?: boolean; | ||
interval?: number; | ||
binaryInterval?: number; | ||
alwaysStat?: boolean; | ||
depth?: number; | ||
awaitWriteFinish?: | ||
| boolean | ||
| { stabilityThreshold?: number; pollInterval?: number }; | ||
}; | ||
|
||
export type WatchEvent = | ||
| { code: 'START' } | ||
| { code: 'CHANGE'; id: string; event: ChangeEvent } | ||
| { code: 'ERROR'; error: Error } | ||
| { code: 'END' }; | ||
|
||
export type EventListener<T, K extends keyof T> = T[K] extends ( | ||
...args: any[] | ||
) => any | ||
? (...args: Parameters<T[K]>) => void | Promise<void> | ||
: never; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { WatchEmitter } from './emitter'; | ||
import { FileWatcher } from './fileWatcher'; | ||
import type { ChangeEvent, ChokidarOptions, WatchEvent } from './types'; | ||
|
||
export interface WatchOptions { | ||
include?: string | RegExp | Array<string | RegExp>; | ||
exclude?: string | RegExp | Array<string | RegExp>; | ||
chokidar?: ChokidarOptions; | ||
} | ||
|
||
export class Watcher { | ||
private fileWatcher: FileWatcher; | ||
private emitter: WatchEmitter<{ | ||
event: (event: WatchEvent) => Promise<void>; | ||
change: (path: string, details: { event: ChangeEvent }) => Promise<void>; | ||
close: () => Promise<void>; | ||
}>; | ||
private closed = false; | ||
|
||
constructor(options: WatchOptions = {}) { | ||
this.emitter = new WatchEmitter(); | ||
this.fileWatcher = new FileWatcher( | ||
(id, event) => this.handleChange(id, event), | ||
options.chokidar, | ||
); | ||
} | ||
|
||
watch(paths: string | string[]): void { | ||
const pathArray = Array.isArray(paths) ? paths : [paths]; | ||
for (const path of pathArray) { | ||
this.fileWatcher.watch(path); | ||
} | ||
} | ||
|
||
on<E extends 'event' | 'change' | 'close'>( | ||
event: E, | ||
callback: E extends 'event' | ||
? (event: WatchEvent) => Promise<void> | ||
: E extends 'change' | ||
? (id: string, details: { event: ChangeEvent }) => Promise<void> | ||
: () => Promise<void>, | ||
): void { | ||
this.emitter.on(event, callback as any); | ||
} | ||
|
||
async close(): Promise<void> { | ||
if (this.closed) return; | ||
this.closed = true; | ||
this.fileWatcher.close(); | ||
await this.emitter.emit('close'); | ||
} | ||
|
||
/** | ||
* 提供两种层次的监听 | ||
* | ||
* 1. event:统一处理所有类型的事件 | ||
* example: | ||
* watcher.on('event', (event) => { | ||
* switch (event.code) { | ||
* case 'START': | ||
* console.log('Watch started'); | ||
* break; | ||
* case 'CHANGE': | ||
* console.log(`File ${event.id} ${event.event}`); | ||
* break; | ||
* case 'ERROR': | ||
* console.error('Watch error:', event.error); | ||
* break; | ||
* case 'END': | ||
* console.log('Watch ended'); | ||
* break; | ||
* } | ||
* }); | ||
* | ||
* 2. change:只关心文件变化 | ||
* example: | ||
* watcher.on('change', (id, {event}) => { | ||
* console.log(`File ${id} ${event}`); | ||
* }); | ||
*/ | ||
private async handleChange(id: string, event: ChangeEvent): Promise<void> { | ||
await this.emitter.emit('event', { code: 'CHANGE', id, event }); | ||
await this.emitter.emit('change', id, { event }); | ||
} | ||
} |