Skip to content

Commit

Permalink
feat(plugin-hooks): add watchChange hook
Browse files Browse the repository at this point in the history
  • Loading branch information
Xy2002 committed Dec 20, 2024
1 parent ac5e8f3 commit d6201eb
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 0 deletions.
19 changes: 19 additions & 0 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { buildHtml } from './html';
import { PluginHookType } from './plugin/plugin_manager';
import { sync } from './sync/sync';
import { type Context } from './types';
import { Watcher } from './watch/watcher';

export async function build({
context,
Expand All @@ -28,6 +29,24 @@ export async function build({

// sync with watch
if (watch) {
const watcher = new Watcher({
chokidar: {
ignoreInitial: true,
},
});

watcher.watch(['./src']);

watcher.on('change', async (id, { event }) => {
await context.pluginManager.apply({
hook: 'watchChange',
args: [id, { event }],
memo: [],
type: PluginHookType.Parallel,
pluginContext: context.pluginContext,
});
});

const pagesDir = path.join(cwd, 'src/pages');
chokidar
.watch(pagesDir, {
Expand Down
9 changes: 9 additions & 0 deletions src/plugin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ export const PluginSchema = z.object({
z.union([z.string(), z.promise(z.string()), z.null()]),
)
.optional(),
watchChange: z
.function(
z.tuple([
z.string(),
z.object({ event: z.enum(['create', 'update', 'delete']) }),
]),
z.void(),
)
.optional(),
});

export type Plugin = z.infer<typeof PluginSchema>;
38 changes: 38 additions & 0 deletions src/watch/emitter.ts
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;
}
}
42 changes: 42 additions & 0 deletions src/watch/fileWatcher.ts
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'));
}
}
30 changes: 30 additions & 0 deletions src/watch/types.ts
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;
85 changes: 85 additions & 0 deletions src/watch/watcher.ts
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 });
}
}

0 comments on commit d6201eb

Please sign in to comment.