diff --git a/.changeset/sharp-starfishes-eat.md b/.changeset/sharp-starfishes-eat.md new file mode 100644 index 00000000..6843d4b3 --- /dev/null +++ b/.changeset/sharp-starfishes-eat.md @@ -0,0 +1,5 @@ +--- +'@ice/pkg': minor +--- + +feat: add batch for watcher change event diff --git a/packages/pkg/src/commands/start.ts b/packages/pkg/src/commands/start.ts index 1dbb645e..b41c3fa5 100644 --- a/packages/pkg/src/commands/start.ts +++ b/packages/pkg/src/commands/start.ts @@ -2,15 +2,14 @@ import consola from 'consola'; import { RollupOptions } from 'rollup'; import { getBuildTasks } from '../helpers/getBuildTasks.js'; import { getRollupOptions } from '../helpers/getRollupOptions.js'; -import { createWatcher } from '../helpers/watcher.js'; +import { createBatchChangeHandler, createWatcher } from '../helpers/watcher.js'; import { watchBundleTasks } from '../tasks/bundle.js'; import { watchTransformTasks } from '../tasks/transform.js'; import type { OutputResult, Context, - WatchEvent, - TaskRunnerContext, + TaskRunnerContext, WatchChangedFile, } from '../types.js'; export default async function start(context: Context) { @@ -34,9 +33,12 @@ export default async function start(context: Context) { }); const watcher = createWatcher(taskConfigs); - watcher.on('add', async (id) => await handleChange(id, 'create')); - watcher.on('change', async (id) => await handleChange(id, 'update')); - watcher.on('unlink', async (id) => await handleChange(id, 'delete')); + const batchHandler = createBatchChangeHandler(runChangedCompile); + batchHandler.beginBlock(); + + watcher.on('add', (id) => batchHandler.onChange(id, 'create')); + watcher.on('change', (id) => batchHandler.onChange(id, 'update')); + watcher.on('unlink', (id) => batchHandler.onChange(id, 'delete')); watcher.on('error', (error) => consola.error(error)); const transformOptions = buildTasks @@ -83,14 +85,16 @@ export default async function start(context: Context) { await applyHook('after.start.compile', outputResults); - async function handleChange(id: string, event: WatchEvent) { + batchHandler.endBlock(); + + async function runChangedCompile(changedFiles: WatchChangedFile[]) { const newOutputResults = []; try { const newTransformOutputResults = transformWatchResult.handleChange ? - await transformWatchResult.handleChange(id, event) : + await transformWatchResult.handleChange(changedFiles) : []; const newBundleOutputResults = bundleWatchResult.handleChange ? - await bundleWatchResult.handleChange(id, event) : + await bundleWatchResult.handleChange(changedFiles) : []; newOutputResults.push( ...newTransformOutputResults, diff --git a/packages/pkg/src/helpers/watcher.ts b/packages/pkg/src/helpers/watcher.ts index 5640b15e..76198b11 100644 --- a/packages/pkg/src/helpers/watcher.ts +++ b/packages/pkg/src/helpers/watcher.ts @@ -1,6 +1,10 @@ import * as chokidar from 'chokidar'; import { unique } from '../utils.js'; -import type { TaskConfig } from '../types.js'; +import type { TaskConfig, WatchChangedFile, WatchEvent } from '../types.js'; + +const WATCH_INTERVAL = 250; + +type WatchCallback = (changedFiles: WatchChangedFile[]) => (Promise); export const createWatcher = (taskConfigs: TaskConfig[]) => { const outputs = unique(taskConfigs.map((taskConfig) => taskConfig.outputDir)); @@ -17,3 +21,62 @@ export const createWatcher = (taskConfigs: TaskConfig[]) => { return watcher; }; + +export function createBatchChangeHandler(changeCallback: WatchCallback) { + let nextChangedFiles: WatchChangedFile[] = []; + let runningTask: Promise | null = null; + let enableBatch = false; + let timer: any = 0; + + async function onChange(id: string, event: WatchEvent) { + nextChangedFiles.push({ path: id, event }); + if (enableBatch) { + return; + } + tryRunTask(); + } + + function tryRunTask() { + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(() => { + runTask(); + timer = null; + }, WATCH_INTERVAL); + } + + function runTask() { + if (!nextChangedFiles.length) { + return; + } + + if (!runningTask) { + const changedFiles = nextChangedFiles; + nextChangedFiles = []; + const task = changeCallback(changedFiles); + runningTask = task.finally(() => { + runningTask = null; + tryRunTask(); + }); + } + } + + + return { + /** + * Block and cache file changes event, do not trigger change handler + */ + beginBlock() { + enableBatch = true; + }, + /** + * Trigger change handler since beginBlock + */ + endBlock() { + enableBatch = false; + tryRunTask(); + }, + onChange, + }; +} diff --git a/packages/pkg/src/tasks/bundle.ts b/packages/pkg/src/tasks/bundle.ts index 35b32c08..75d22fd1 100644 --- a/packages/pkg/src/tasks/bundle.ts +++ b/packages/pkg/src/tasks/bundle.ts @@ -35,10 +35,10 @@ export const watchBundleTasks: RunTasks = async (taskOptionsList, context, watch handleChangeFunctions.push(handleChange); } - const handleChange: HandleChange = async (id, event) => { + const handleChange: HandleChange = async (changedFiles) => { const newOutputResults: OutputResult[] = []; for (const handleChangeFunction of handleChangeFunctions) { - const newOutputResult = await handleChangeFunction(id, event); + const newOutputResult = await handleChangeFunction(changedFiles); newOutputResults.push(newOutputResult); } @@ -215,16 +215,18 @@ async function rawWatch( } }; - const handleChange: HandleChange = async (id: string, event: string) => { + const handleChange: HandleChange = async (changedFiles) => { const changeStart = performance.now(); logger.debug('Bundle start...'); for (const task of watcher.tasks) { - task.invalidate(id, { - event, - isTransformDependency: false, - }); + for (const file of changedFiles) { + task.invalidate(file.path, { + event: file.event, + isTransformDependency: false, + }); + } } const outputResult = await getOutputResult(); diff --git a/packages/pkg/src/tasks/transform.ts b/packages/pkg/src/tasks/transform.ts index f97ff418..cf63f89c 100644 --- a/packages/pkg/src/tasks/transform.ts +++ b/packages/pkg/src/tasks/transform.ts @@ -40,17 +40,17 @@ export const watchTransformTasks: RunTasks = async (taskOptionsList, context, wa } outputResults.push(outputResult); - handleChangeFunctions.push(async (id, event) => { - if (event === 'update' || event === 'create') { - return await runTransform(rollupOptions, taskRunnerContext, context, id); + handleChangeFunctions.push(async (changedFiles) => { + if (changedFiles.some((file) => file.event === 'update' || file.event === 'create')) { + return await runTransform(rollupOptions, taskRunnerContext, context, changedFiles.map((file) => file.path)); } }); } - const handleChange: HandleChange = async (id, event) => { + const handleChange: HandleChange = async (changedFiles) => { const newOutputResults: OutputResult[] = []; for (const handleChangeFunction of handleChangeFunctions) { - const newOutputResult = await handleChangeFunction(id, event); + const newOutputResult = await handleChangeFunction(changedFiles); newOutputResults.push(newOutputResult); } @@ -81,7 +81,7 @@ async function runTransform( rollupOptions: RollupOptions, taskRunnerContext: TaskRunnerContext, context: Context, - updatedFile?: string, + updatedFiles?: string[], ): Promise { let isDistContainingSWCHelpers = false; let isDistContainingJSXRuntime = false; @@ -100,11 +100,13 @@ async function runTransform( const files: OutputFile[] = []; - if (updatedFile) { - for (const entryDir of entryDirs) { - if (updatedFile.startsWith(entryDir)) { - files.push(getFileInfo(updatedFile, entryDir)); - break; + if (updatedFiles) { + for (const updatedFile of updatedFiles) { + for (const entryDir of entryDirs) { + if (updatedFile.startsWith(entryDir)) { + files.push(getFileInfo(updatedFile, entryDir)); + break; + } } } } else { diff --git a/packages/pkg/src/types.ts b/packages/pkg/src/types.ts index ebab8915..bb4cd7df 100644 --- a/packages/pkg/src/types.ts +++ b/packages/pkg/src/types.ts @@ -305,7 +305,13 @@ export interface OutputResult { export type NodeEnvMode = 'development' | 'production' | string; export type WatchEvent = 'create' | 'update' | 'delete'; -export type HandleChange = (id: string, event: WatchEvent) => Promise; + +export interface WatchChangedFile { + path: string; + event: WatchEvent; +} + +export type HandleChange = (changedFiles: WatchChangedFile[]) => Promise; export interface TaskResult { handleChange?: HandleChange;