From ae79c3b68afd97824796dc1d2c120cb102450324 Mon Sep 17 00:00:00 2001 From: Ivan Plesskih Date: Tue, 27 Oct 2020 21:40:00 +0500 Subject: [PATCH 1/7] Improved watcher hooks --- src/Graph.ts | 5 +- src/rollup/types.d.ts | 8 ++- src/utils/PluginContext.ts | 1 + src/utils/PluginDriver.ts | 1 + src/watch/fileWatcher.ts | 6 +- src/watch/fsevents-importer.ts | 1 + src/watch/watch.ts | 19 +++-- test/watch/index.js | 128 +++++++++++++++++++++++++++++++++ 8 files changed, 157 insertions(+), 12 deletions(-) diff --git a/src/Graph.ts b/src/Graph.ts index ebbc12b6f8f..9db8f1d3dd1 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -84,10 +84,13 @@ export default class Graph { if (watcher) { this.watchMode = true; - const handleChange = (id: string) => this.pluginDriver.hookSeqSync('watchChange', [id]); + const handleChange = (id: string, del: boolean) => this.pluginDriver.hookSeqSync('watchChange', [id, del]); + const handleClose = () => this.pluginDriver.hookSeqSync('closeWatcher', []); watcher.on('change', handleChange); + watcher.on('close', handleClose); watcher.once('restart', () => { watcher.removeListener('change', handleChange); + watcher.removeListener('close', handleClose); }); } this.pluginDriver = new PluginDriver(this, options, options.plugins, this.pluginCache); diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 7e7c4f1778f..1007e63b3ee 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -195,6 +195,7 @@ export interface PluginContext extends MinimalPluginContext { getFileName: (fileReferenceId: string) => string; getModuleIds: () => IterableIterator; getModuleInfo: GetModuleInfo; + getWatchFiles: () => string[]; /** @deprecated Use `this.resolve` instead */ isExternal: IsExternal; /** @deprecated Use `this.getModuleIds` instead */ @@ -345,13 +346,14 @@ export interface OutputBundleWithPlaceholders { export interface PluginHooks extends OutputPluginHooks { buildEnd: (this: PluginContext, err?: Error) => Promise | void; buildStart: (this: PluginContext, options: NormalizedInputOptions) => Promise | void; + closeWatcher: (this: PluginContext) => void; load: LoadHook; moduleParsed: ModuleParsedHook; options: (this: MinimalPluginContext, options: InputOptions) => InputOptions | null | undefined; resolveDynamicImport: ResolveDynamicImportHook; resolveId: ResolveIdHook; transform: TransformHook; - watchChange: (id: string) => void; + watchChange: (this: PluginContext, id: string, isDeleted: boolean) => void; } interface OutputPluginHooks { @@ -419,6 +421,7 @@ export type FirstPluginHooks = export type SequentialPluginHooks = | 'augmentChunkHash' + | 'closeWatcher' | 'generateBundle' | 'options' | 'outputOptions' @@ -816,7 +819,8 @@ export type RollupWatcherEvent = export interface RollupWatcher extends TypedEventEmitter<{ - change: (id: string) => void; + change: (id: string, isDeleted: boolean) => void; + close: () => void; event: (event: RollupWatcherEvent) => void; restart: () => void; }> { diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts index 215225e5c0c..2f245b6c0c3 100644 --- a/src/utils/PluginContext.ts +++ b/src/utils/PluginContext.ts @@ -126,6 +126,7 @@ export function getPluginContexts( getFileName: fileEmitter.getFileName, getModuleIds: () => graph.modulesById.keys(), getModuleInfo: graph.getModuleInfo, + getWatchFiles: () => Object.keys(graph.watchFiles), isExternal: getDeprecatedContextHandler( (id: string, parentId: string | undefined, isResolved = false) => options.external(id, parentId, isResolved), diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index b688a556295..b701decca16 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -47,6 +47,7 @@ const inputHookNames: { } = { buildEnd: 1, buildStart: 1, + closeWatcher: 1, load: 1, moduleParsed: 1, options: 1, diff --git a/src/watch/fileWatcher.ts b/src/watch/fileWatcher.ts index d168df9f0b1..75cd85cf821 100644 --- a/src/watch/fileWatcher.ts +++ b/src/watch/fileWatcher.ts @@ -45,7 +45,7 @@ export class FileWatcher { const task = this.task; const isLinux = platform() === 'linux'; const isTransformDependency = transformWatcherId !== null; - const handleChange = (id: string) => { + const handleChange = (id: string, _: unknown, isDeleted = false) => { const changedId = transformWatcherId || id; if (isLinux) { // unwatching and watching fixes an issue with chokidar where on certain systems, @@ -54,13 +54,13 @@ export class FileWatcher { watcher.unwatch(changedId); watcher.add(changedId); } - task.invalidate(changedId, isTransformDependency); + task.invalidate(changedId, isTransformDependency, isDeleted); }; const watcher = chokidar .watch([], this.chokidarOptions) .on('add', handleChange) .on('change', handleChange) - .on('unlink', handleChange); + .on('unlink', id => handleChange(id, null, true)); return watcher; } } diff --git a/src/watch/fsevents-importer.ts b/src/watch/fsevents-importer.ts index d9ee8bc60d4..db46339b470 100644 --- a/src/watch/fsevents-importer.ts +++ b/src/watch/fsevents-importer.ts @@ -2,6 +2,7 @@ let fsEvents: any; let fsEventsImportError: any; export function loadFsEvents() { + // @ts-ignore return import('fsevents') .then(namespace => { fsEvents = namespace.default; diff --git a/src/watch/watch.ts b/src/watch/watch.ts index 0e894be2c07..d602b5e5fad 100644 --- a/src/watch/watch.ts +++ b/src/watch/watch.ts @@ -18,6 +18,7 @@ export class Watcher { private buildDelay = 0; private buildTimeout: NodeJS.Timer | null = null; + private deletedIds: Set = new Set(); private invalidatedIds: Set = new Set(); private rerun = false; private running: boolean; @@ -43,16 +44,21 @@ export class Watcher { for (const task of this.tasks) { task.close(); } + this.emit('close') this.emitter.removeAllListeners(); } - emit(event: string, value?: any) { - this.emitter.emit(event as any, value); + emit(event: string, ...args: unknown[]) { + this.emitter.emit(event as any, ...args); } - invalidate(id?: string) { + invalidate(id?: string, isDeleted?: boolean) { if (id) { this.invalidatedIds.add(id); + if (isDeleted) + this.deletedIds.add(id); + else + this.deletedIds.delete(id) } if (this.running) { this.rerun = true; @@ -64,9 +70,10 @@ export class Watcher { this.buildTimeout = setTimeout(() => { this.buildTimeout = null; for (const id of this.invalidatedIds) { - this.emit('change', id); + this.emit('change', id, this.deletedIds.has(id)); } this.invalidatedIds.clear(); + this.deletedIds.clear(); this.emit('restart'); this.run(); }, this.buildDelay); @@ -144,7 +151,7 @@ export class Task { this.fileWatcher.close(); } - invalidate(id: string, isTransformDependency: boolean | undefined) { + invalidate(id: string, isTransformDependency: boolean | undefined, isDeleted: boolean) { this.invalidated = true; if (isTransformDependency) { for (const module of this.cache.modules) { @@ -153,7 +160,7 @@ export class Task { module.originalCode = null as any; } } - this.watcher.invalidate(id); + this.watcher.invalidate(id, isDeleted); } async run() { diff --git a/test/watch/index.js b/test/watch/index.js index bebcd586515..e313b763d8a 100644 --- a/test/watch/index.js +++ b/test/watch/index.js @@ -237,6 +237,134 @@ describe('rollup.watch', () => { }); }); + it('passes isDeleted parameter to the watchChange plugin hook', () => { + const deleted = []; + let ids; + const expectedIds = ['test/_tmp/input/watched', path.resolve('test/_tmp/input/main.js')]; + return sander + .copydir('test/watch/samples/watch-files') + .to('test/_tmp/input') + .then(() => { + watcher = rollup.watch({ + input: 'test/_tmp/input/main.js', + output: { + file: 'test/_tmp/output/bundle.js', + format: 'cjs', + exports: 'auto' + }, + plugins: { + buildStart() { + this.addWatchFile('test/_tmp/input/watched'); + }, + watchChange(id, isDeleted) { + assert.strictEqual(id, 'test/_tmp/input/watched'); + deleted.push(isDeleted); + }, + buildEnd() { + ids = this.getWatchFiles(); + } + } + }); + + return sequence(watcher, [ + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + assert.deepStrictEqual(deleted, []); + assert.deepStrictEqual(ids, expectedIds); + sander.writeFileSync('test/_tmp/input/watched', 'another'); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + assert.deepStrictEqual(deleted, [false]); + assert.deepStrictEqual(ids, expectedIds); + sander.rimrafSync('test/_tmp/input/watched'); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + assert.deepStrictEqual(deleted, [false, true]); + assert.deepStrictEqual(ids, expectedIds); + sander.writeFileSync('test/_tmp/input/watched', 'third'); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + assert.deepStrictEqual(deleted, [false, true, false]); + assert.deepStrictEqual(ids, expectedIds); + } + ]); + }); + }); + + it('calls closeWatcher plugin hook', () => { + let calls = 0; + let ctx1; + let ctx2; + return sander + .copydir('test/watch/samples/basic') + .to('test/_tmp/input') + .then(() => { + watcher = rollup.watch({ + input: 'test/_tmp/input/main.js', + output: { + file: 'test/_tmp/output/bundle.js', + format: 'cjs', + exports: 'auto' + }, + plugins: [ + { + buildStart() { + ctx1 = this; + }, + closeWatcher() { + assert.strictEqual(ctx1, this); + calls++; + } + }, + { + buildStart() { + ctx2 = this; + }, + closeWatcher() { + assert.strictEqual(ctx2, this); + calls++; + } + }, + ] + }); + + return sequence(watcher, [ + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + assert.ok(ctx1); + assert.ok(ctx2); + watcher.once('close', () => { + assert.strictEqual(calls, 2); + }); + watcher.close(); + }, + ]); + }); + }); + it('watches a file in code-splitting mode', () => { return sander .copydir('test/watch/samples/code-splitting') From 11bda7be7b0482333466c14556a68442c00dbacf Mon Sep 17 00:00:00 2001 From: Ivan Plesskih Date: Tue, 27 Oct 2020 22:15:21 +0500 Subject: [PATCH 2/7] Updated docs --- docs/05-plugin-development.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index 80e26ff4745..46bd83d6dd8 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -92,6 +92,13 @@ Next Hook: [`resolveId`](guide/en/#resolveid) to resolve each entry point in par Called on each `rollup.rollup` build. This is the recommended hook to use when you need access to the options passed to `rollup.rollup()` as it takes the transformations by all [`options`](guide/en/#options) hooks into account and also contains the right default values for unset options. +#### `closeWatcher` +Type: `() => void`
+Kind: `sync, sequential`
+Previous/Next Hook: This hook can be triggered at any time both during the build and the output generation phases. If that is the case, the current build will still proceed but no new [`watchChange`](guide/en/#watchChange) events will be triggered ever. + +Notifies a plugin when watcher process closes and all open resources should be closed too. This hook cannot be used by output plugins. + #### `load` Type: `(id: string) => string | null | {code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | "no-treeshake" | null, syntheticNamedExports?: boolean | string | null, meta?: {[plugin: string]: any} | null}`
Kind: `async, first`
@@ -236,11 +243,12 @@ See [custom module meta-data](guide/en/#custom-module-meta-data) for how to use You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--moduleinfo--null) to find out the previous values of `moduleSideEffects`, `syntheticNamedExports` and `meta` inside this hook. #### `watchChange` -Type: `(id: string) => void`
+Type: `(id: string, isDeleted: boolean) => void`
Kind: `sync, sequential`
Previous/Next Hook: This hook can be triggered at any time both during the build and the output generation phases. If that is the case, the current build will still proceed but a new build will be scheduled to start once the current build has completed, starting again with [`options`](guide/en/#options). Notifies a plugin whenever rollup has detected a change to a monitored file in `--watch` mode. This hook cannot be used by output plugins. +Second argument will be `true` if file deleted. ### Output Generation Hooks @@ -655,6 +663,11 @@ During the build, this object represents currently available information about t Returns `null` if the module id cannot be found. +#### `this.getWatchFiles() => string[]` + +Get ids of the files which has been watched previously. Include both files added by plugins with `this.addWatchFile` + and files added implicitly by rollup during the build. + #### `this.meta: {rollupVersion: string, watchMode: boolean}` An object containing potentially useful Rollup metadata: From be616beabc136142b32ea616734926b31b12adca Mon Sep 17 00:00:00 2001 From: Ivan Plesskih Date: Wed, 28 Oct 2020 18:20:51 +0500 Subject: [PATCH 3/7] Replace isDeleted with change details, updated docs and added test for build delay --- docs/05-plugin-development.md | 4 +- src/Graph.ts | 5 ++- src/rollup/types.d.ts | 15 ++++--- src/watch/fileWatcher.ts | 12 +++--- src/watch/watch.ts | 71 ++++++++++++++++++++------------ test/watch/index.js | 76 +++++++++++++++++++++++++++++++---- 6 files changed, 134 insertions(+), 49 deletions(-) diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index 46bd83d6dd8..0077a185717 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -243,12 +243,12 @@ See [custom module meta-data](guide/en/#custom-module-meta-data) for how to use You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--moduleinfo--null) to find out the previous values of `moduleSideEffects`, `syntheticNamedExports` and `meta` inside this hook. #### `watchChange` -Type: `(id: string, isDeleted: boolean) => void`
+Type: `watchChange: (id: string, change: {event: 'create' | 'update' | 'delete'}) => void`
Kind: `sync, sequential`
Previous/Next Hook: This hook can be triggered at any time both during the build and the output generation phases. If that is the case, the current build will still proceed but a new build will be scheduled to start once the current build has completed, starting again with [`options`](guide/en/#options). Notifies a plugin whenever rollup has detected a change to a monitored file in `--watch` mode. This hook cannot be used by output plugins. -Second argument will be `true` if file deleted. +Second argument contains additional details of change event. ### Output Generation Hooks diff --git a/src/Graph.ts b/src/Graph.ts index 9db8f1d3dd1..369146adb97 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -10,7 +10,8 @@ import { NormalizedInputOptions, RollupCache, RollupWatcher, - SerializablePluginCache + SerializablePluginCache, + WatchChangeHook } from './rollup/types'; import { BuildPhase } from './utils/buildPhase'; import { errImplicitDependantIsNotIncluded, error } from './utils/error'; @@ -84,7 +85,7 @@ export default class Graph { if (watcher) { this.watchMode = true; - const handleChange = (id: string, del: boolean) => this.pluginDriver.hookSeqSync('watchChange', [id, del]); + const handleChange: WatchChangeHook = (...args) => this.pluginDriver.hookSeqSync('watchChange', args); const handleClose = () => this.pluginDriver.hookSeqSync('closeWatcher', []); watcher.on('change', handleChange); watcher.on('close', handleClose); diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 1007e63b3ee..cffa45c40d4 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -319,6 +319,9 @@ export type ResolveFileUrlHook = ( export type AddonHookFunction = (this: PluginContext) => string | Promise; export type AddonHook = string | AddonHookFunction; +export type ChangeEvent = 'create' | 'update' | 'delete' +export type WatchChangeHook = (this: PluginContext, id: string, change: {event: ChangeEvent}) => void + /** * use this type for plugin annotation * @example @@ -353,7 +356,7 @@ export interface PluginHooks extends OutputPluginHooks { resolveDynamicImport: ResolveDynamicImportHook; resolveId: ResolveIdHook; transform: TransformHook; - watchChange: (this: PluginContext, id: string, isDeleted: boolean) => void; + watchChange: WatchChangeHook; } interface OutputPluginHooks { @@ -786,9 +789,9 @@ export interface RollupWatchOptions extends InputOptions { watch?: WatcherOptions | false; } -interface TypedEventEmitter { +interface TypedEventEmitter any}> { addListener(event: K, listener: T[K]): this; - emit(event: K, ...args: any[]): boolean; + emit(event: K, ...args: Parameters): boolean; eventNames(): Array; getMaxListeners(): number; listenerCount(type: keyof T): number; @@ -806,11 +809,11 @@ interface TypedEventEmitter { export type RollupWatcherEvent = | { code: 'START' } - | { code: 'BUNDLE_START'; input: InputOption; output: readonly string[] } + | { code: 'BUNDLE_START'; input?: InputOption; output: readonly string[] } | { code: 'BUNDLE_END'; duration: number; - input: InputOption; + input?: InputOption; output: readonly string[]; result: RollupBuild; } @@ -819,7 +822,7 @@ export type RollupWatcherEvent = export interface RollupWatcher extends TypedEventEmitter<{ - change: (id: string, isDeleted: boolean) => void; + change: WatchChangeHook; close: () => void; event: (event: RollupWatcherEvent) => void; restart: () => void; diff --git a/src/watch/fileWatcher.ts b/src/watch/fileWatcher.ts index 75cd85cf821..02d40ed5bf3 100644 --- a/src/watch/fileWatcher.ts +++ b/src/watch/fileWatcher.ts @@ -1,6 +1,6 @@ import chokidar, { FSWatcher } from 'chokidar'; import { platform } from 'os'; -import { ChokidarOptions } from '../rollup/types'; +import { ChangeEvent, ChokidarOptions } from '../rollup/types'; import { Task } from './watch'; export class FileWatcher { @@ -45,7 +45,7 @@ export class FileWatcher { const task = this.task; const isLinux = platform() === 'linux'; const isTransformDependency = transformWatcherId !== null; - const handleChange = (id: string, _: unknown, isDeleted = false) => { + const handleChange = (id: string, event: ChangeEvent) => { const changedId = transformWatcherId || id; if (isLinux) { // unwatching and watching fixes an issue with chokidar where on certain systems, @@ -54,13 +54,13 @@ export class FileWatcher { watcher.unwatch(changedId); watcher.add(changedId); } - task.invalidate(changedId, isTransformDependency, isDeleted); + task.invalidate(changedId, {isTransformDependency, event}); }; const watcher = chokidar .watch([], this.chokidarOptions) - .on('add', handleChange) - .on('change', handleChange) - .on('unlink', id => handleChange(id, null, true)); + .on('add', id => handleChange(id, 'create')) + .on('change', id => handleChange(id, 'update')) + .on('unlink', id => handleChange(id, 'delete')); return watcher; } } diff --git a/src/watch/watch.ts b/src/watch/watch.ts index d602b5e5fad..b10bbdaa5a1 100644 --- a/src/watch/watch.ts +++ b/src/watch/watch.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import createFilter from 'rollup-pluginutils/src/createFilter'; import { rollupInternal } from '../rollup/rollup'; import { + ChangeEvent, MergedRollupOptions, OutputOptions, RollupBuild, @@ -13,13 +14,30 @@ import { mergeOptions } from '../utils/options/mergeOptions'; import { GenericConfigObject } from '../utils/options/options'; import { FileWatcher } from './fileWatcher'; +const eventsRewrites: Record> = { + create: { + create: 'buggy', + delete: null, //delete file from map + update: 'create', + }, + delete: { + create: 'update', + delete: 'buggy', + update: 'buggy', + }, + update: { + create: 'buggy', + delete: 'delete', + update: 'update', + } +} + export class Watcher { emitter: RollupWatcher; private buildDelay = 0; private buildTimeout: NodeJS.Timer | null = null; - private deletedIds: Set = new Set(); - private invalidatedIds: Set = new Set(); + private invalidatedIds: Map = new Map(); private rerun = false; private running: boolean; private tasks: Task[]; @@ -44,21 +62,25 @@ export class Watcher { for (const task of this.tasks) { task.close(); } - this.emit('close') + this.emitter.emit('close'); this.emitter.removeAllListeners(); } - emit(event: string, ...args: unknown[]) { - this.emitter.emit(event as any, ...args); - } + invalidate(file?: {event: ChangeEvent, id: string}) { + if (file) { + const prevEvent = this.invalidatedIds.get(file.id); + const event = prevEvent + ? eventsRewrites[prevEvent][file.event] + : file.event; - invalidate(id?: string, isDeleted?: boolean) { - if (id) { - this.invalidatedIds.add(id); - if (isDeleted) - this.deletedIds.add(id); - else - this.deletedIds.delete(id) + if (event === 'buggy') { + //TODO: throws or warn? Currently just ignore, uses new event + this.invalidatedIds.set(file.id, file.event); + } else if (event === null) { + this.invalidatedIds.delete(file.id); + } else { + this.invalidatedIds.set(file.id, event); + } } if (this.running) { this.rerun = true; @@ -69,12 +91,11 @@ export class Watcher { this.buildTimeout = setTimeout(() => { this.buildTimeout = null; - for (const id of this.invalidatedIds) { - this.emit('change', id, this.deletedIds.has(id)); + for (const [id, event] of this.invalidatedIds.entries()) { + this.emitter.emit('change', id, {event}); } this.invalidatedIds.clear(); - this.deletedIds.clear(); - this.emit('restart'); + this.emitter.emit('restart'); this.run(); }, this.buildDelay); } @@ -82,7 +103,7 @@ export class Watcher { private async run() { this.running = true; - this.emit('event', { + this.emitter.emit('event', { code: 'START' }); @@ -91,12 +112,12 @@ export class Watcher { await task.run(); } this.running = false; - this.emit('event', { + this.emitter.emit('event', { code: 'END' }); } catch (error) { this.running = false; - this.emit('event', { + this.emitter.emit('event', { code: 'ERROR', error }); @@ -151,16 +172,16 @@ export class Task { this.fileWatcher.close(); } - invalidate(id: string, isTransformDependency: boolean | undefined, isDeleted: boolean) { + invalidate(id: string, details: {event: ChangeEvent, isTransformDependency?: boolean}) { this.invalidated = true; - if (isTransformDependency) { + if (details.isTransformDependency) { for (const module of this.cache.modules) { if (module.transformDependencies.indexOf(id) === -1) continue; // effective invalidation module.originalCode = null as any; } } - this.watcher.invalidate(id, isDeleted); + this.watcher.invalidate({id, event: details.event}); } async run() { @@ -174,7 +195,7 @@ export class Task { const start = Date.now(); - this.watcher.emit('event', { + this.watcher.emitter.emit('event', { code: 'BUNDLE_START', input: this.options.input, output: this.outputFiles @@ -187,7 +208,7 @@ export class Task { } this.updateWatchedFiles(result); this.skipWrite || (await Promise.all(this.outputs.map(output => result.write(output)))); - this.watcher.emit('event', { + this.watcher.emitter.emit('event', { code: 'BUNDLE_END', duration: Date.now() - start, input: this.options.input, diff --git a/test/watch/index.js b/test/watch/index.js index e313b763d8a..1989d34444d 100644 --- a/test/watch/index.js +++ b/test/watch/index.js @@ -237,8 +237,8 @@ describe('rollup.watch', () => { }); }); - it('passes isDeleted parameter to the watchChange plugin hook', () => { - const deleted = []; + it('passes change parameter to the watchChange plugin hook', () => { + const events = []; let ids; const expectedIds = ['test/_tmp/input/watched', path.resolve('test/_tmp/input/main.js')]; return sander @@ -256,9 +256,9 @@ describe('rollup.watch', () => { buildStart() { this.addWatchFile('test/_tmp/input/watched'); }, - watchChange(id, isDeleted) { + watchChange(id, {event}) { assert.strictEqual(id, 'test/_tmp/input/watched'); - deleted.push(isDeleted); + events.push(event); }, buildEnd() { ids = this.getWatchFiles(); @@ -273,7 +273,7 @@ describe('rollup.watch', () => { 'END', () => { assert.strictEqual(run('../_tmp/output/bundle.js'), 42); - assert.deepStrictEqual(deleted, []); + assert.deepStrictEqual(events, []); assert.deepStrictEqual(ids, expectedIds); sander.writeFileSync('test/_tmp/input/watched', 'another'); }, @@ -283,7 +283,7 @@ describe('rollup.watch', () => { 'END', () => { assert.strictEqual(run('../_tmp/output/bundle.js'), 42); - assert.deepStrictEqual(deleted, [false]); + assert.deepStrictEqual(events, ['update']); assert.deepStrictEqual(ids, expectedIds); sander.rimrafSync('test/_tmp/input/watched'); }, @@ -293,7 +293,7 @@ describe('rollup.watch', () => { 'END', () => { assert.strictEqual(run('../_tmp/output/bundle.js'), 42); - assert.deepStrictEqual(deleted, [false, true]); + assert.deepStrictEqual(events, ['update', 'delete']); assert.deepStrictEqual(ids, expectedIds); sander.writeFileSync('test/_tmp/input/watched', 'third'); }, @@ -303,13 +303,73 @@ describe('rollup.watch', () => { 'END', () => { assert.strictEqual(run('../_tmp/output/bundle.js'), 42); - assert.deepStrictEqual(deleted, [false, true, false]); + assert.deepStrictEqual(events, ['update', 'delete', 'create']); assert.deepStrictEqual(ids, expectedIds); } ]); }); }); + it('correctly rewrites change event during build delay', function() { + this.timeout(3000); + let e; + return sander + .copydir('test/watch/samples/watch-files') + .to('test/_tmp/input') + .then(() => { + watcher = rollup.watch({ + input: 'test/_tmp/input/main.js', + output: { + file: 'test/_tmp/output/bundle.js', + format: 'cjs', + exports: 'auto' + }, + watch: { + buildDelay: 300, + }, + plugins: { + buildStart() { + this.addWatchFile('test/_tmp/input/watched'); + }, + watchChange(id, {event}) { + assert.strictEqual(id, 'test/_tmp/input/watched'); + e = event; + } + } + }); + + return sequence(watcher, [ + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + assert.strictEqual(e, undefined); + sander.writeFileSync('test/_tmp/input/watched', 'another'); + //default chokidar timeout for waiting is 100, so we waiting 150 + setTimeout(() => sander.rimrafSync('test/_tmp/input/watched'), 150); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(e, 'delete'); + sander.writeFileSync('test/_tmp/input/watched', '123'); + setTimeout(() => sander.writeFileSync('test/_tmp/input/watched', 'asd'), 150); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(e, 'create'); + } + ]); + }); + }); + it('calls closeWatcher plugin hook', () => { let calls = 0; let ctx1; From be1bec6fd81e5e703de0622a8c79f5a5dfdf6401 Mon Sep 17 00:00:00 2001 From: Ivan Plesskih Date: Wed, 28 Oct 2020 18:48:38 +0500 Subject: [PATCH 4/7] Fixed tests --- test/watch/index.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/watch/index.js b/test/watch/index.js index 1989d34444d..7fd6aeee696 100644 --- a/test/watch/index.js +++ b/test/watch/index.js @@ -310,8 +310,7 @@ describe('rollup.watch', () => { }); }); - it('correctly rewrites change event during build delay', function() { - this.timeout(3000); + it('correctly rewrites change event during build delay', () => { let e; return sander .copydir('test/watch/samples/watch-files') @@ -325,7 +324,10 @@ describe('rollup.watch', () => { exports: 'auto' }, watch: { - buildDelay: 300, + buildDelay: 100, + chokidar: { + atomic: true, + } }, plugins: { buildStart() { @@ -347,8 +349,7 @@ describe('rollup.watch', () => { assert.strictEqual(run('../_tmp/output/bundle.js'), 42); assert.strictEqual(e, undefined); sander.writeFileSync('test/_tmp/input/watched', 'another'); - //default chokidar timeout for waiting is 100, so we waiting 150 - setTimeout(() => sander.rimrafSync('test/_tmp/input/watched'), 150); + setTimeout(() => sander.rimrafSync('test/_tmp/input/watched'), 10); }, 'START', 'BUNDLE_START', @@ -357,7 +358,7 @@ describe('rollup.watch', () => { () => { assert.strictEqual(e, 'delete'); sander.writeFileSync('test/_tmp/input/watched', '123'); - setTimeout(() => sander.writeFileSync('test/_tmp/input/watched', 'asd'), 150); + setTimeout(() => sander.writeFileSync('test/_tmp/input/watched', 'asd'), 10); }, 'START', 'BUNDLE_START', From c9e23fa6ce16b0d8db77ebb9dff4d40e180273f2 Mon Sep 17 00:00:00 2001 From: Ivan Plesskih Date: Wed, 28 Oct 2020 19:20:44 +0500 Subject: [PATCH 5/7] Fixed tests again --- test/watch/index.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/test/watch/index.js b/test/watch/index.js index 7fd6aeee696..67a21a26632 100644 --- a/test/watch/index.js +++ b/test/watch/index.js @@ -310,7 +310,8 @@ describe('rollup.watch', () => { }); }); - it('correctly rewrites change event during build delay', () => { + it('correctly rewrites change event during build delay', function() { + this.timeout(3000); let e; return sander .copydir('test/watch/samples/watch-files') @@ -324,9 +325,9 @@ describe('rollup.watch', () => { exports: 'auto' }, watch: { - buildDelay: 100, + buildDelay: 150, chokidar: { - atomic: true, + atomic: 30, } }, plugins: { @@ -349,7 +350,7 @@ describe('rollup.watch', () => { assert.strictEqual(run('../_tmp/output/bundle.js'), 42); assert.strictEqual(e, undefined); sander.writeFileSync('test/_tmp/input/watched', 'another'); - setTimeout(() => sander.rimrafSync('test/_tmp/input/watched'), 10); + setTimeout(() => sander.rimrafSync('test/_tmp/input/watched'), 50); }, 'START', 'BUNDLE_START', @@ -357,8 +358,18 @@ describe('rollup.watch', () => { 'END', () => { assert.strictEqual(e, 'delete'); + e = undefined; sander.writeFileSync('test/_tmp/input/watched', '123'); - setTimeout(() => sander.writeFileSync('test/_tmp/input/watched', 'asd'), 10); + setTimeout(() => sander.rimrafSync('test/_tmp/input/watched'), 50); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(e, undefined); + sander.writeFileSync('test/_tmp/input/watched', '123'); + setTimeout(() => sander.writeFileSync('test/_tmp/input/watched', 'asd'), 50); }, 'START', 'BUNDLE_START', From 34d14f034ceaacaa26a6758eab230f57a543538d Mon Sep 17 00:00:00 2001 From: Ivan Plesskih Date: Fri, 30 Oct 2020 15:42:46 +0500 Subject: [PATCH 6/7] Minor fixes --- docs/05-plugin-development.md | 6 ++---- src/rollup/types.d.ts | 2 +- src/watch/fsevents-importer.ts | 1 - typings/declarations.d.ts | 4 ++++ 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index 0077a185717..60f5d969c96 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -247,8 +247,7 @@ Type: `watchChange: (id: string, change: {event: 'create' | 'update' | 'delete'} Kind: `sync, sequential`
Previous/Next Hook: This hook can be triggered at any time both during the build and the output generation phases. If that is the case, the current build will still proceed but a new build will be scheduled to start once the current build has completed, starting again with [`options`](guide/en/#options). -Notifies a plugin whenever rollup has detected a change to a monitored file in `--watch` mode. This hook cannot be used by output plugins. -Second argument contains additional details of change event. +Notifies a plugin whenever rollup has detected a change to a monitored file in `--watch` mode. This hook cannot be used by output plugins. Second argument contains additional details of change event. ### Output Generation Hooks @@ -665,8 +664,7 @@ Returns `null` if the module id cannot be found. #### `this.getWatchFiles() => string[]` -Get ids of the files which has been watched previously. Include both files added by plugins with `this.addWatchFile` - and files added implicitly by rollup during the build. +Get ids of the files which has been watched previously. Include both files added by plugins with `this.addWatchFile` and files added implicitly by rollup during the build. #### `this.meta: {rollupVersion: string, watchMode: boolean}` diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index cffa45c40d4..540ef51a48b 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -822,7 +822,7 @@ export type RollupWatcherEvent = export interface RollupWatcher extends TypedEventEmitter<{ - change: WatchChangeHook; + change: (id: string, change: {event: ChangeEvent}) => void; close: () => void; event: (event: RollupWatcherEvent) => void; restart: () => void; diff --git a/src/watch/fsevents-importer.ts b/src/watch/fsevents-importer.ts index db46339b470..d9ee8bc60d4 100644 --- a/src/watch/fsevents-importer.ts +++ b/src/watch/fsevents-importer.ts @@ -2,7 +2,6 @@ let fsEvents: any; let fsEventsImportError: any; export function loadFsEvents() { - // @ts-ignore return import('fsevents') .then(namespace => { fsEvents = namespace.default; diff --git a/typings/declarations.d.ts b/typings/declarations.d.ts index 9c8c6deb27c..441102c383c 100644 --- a/typings/declarations.d.ts +++ b/typings/declarations.d.ts @@ -30,3 +30,7 @@ declare module 'acorn-numeric-separator' { const plugin: (BaseParser: typeof acorn.Parser) => typeof acorn.Parser; export default plugin; } + +declare module 'fsevents' { + export default {}; +} From 4891dddaa51f3f885fa20c63873b2b9d5a1d79c7 Mon Sep 17 00:00:00 2001 From: Ivan Plesskih Date: Fri, 30 Oct 2020 16:13:44 +0500 Subject: [PATCH 7/7] Added closeWatcher to hooks overview --- docs/05-plugin-development.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index 60f5d969c96..9642278864f 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -72,7 +72,9 @@ To interact with the build process, your plugin object includes 'hooks'. Hooks a * `sequential`: If several plugins implement this hook, all of them will be run in the specified plugin order. If a hook is async, subsequent hooks of this kind will wait until the current hook is resolved. * `parallel`: If several plugins implement this hook, all of them will be run in the specified plugin order. If a hook is async, subsequent hooks of this kind will be run in parallel and not wait for the current hook. -Build hooks are run during the build phase, which is triggered by `rollup.rollup(inputOptions)`. They are mainly concerned with locating, providing and transforming input files before they are processed by Rollup. The first hook of the build phase is [options](guide/en/#options), the last one is always [buildEnd](guide/en/#buildend). Additionally in watch mode, the [watchChange](guide/en/#watchchange) hook can be triggered at any time to notify a new run will be triggered once the current run has generated its outputs. +Build hooks are run during the build phase, which is triggered by `rollup.rollup(inputOptions)`. They are mainly concerned with locating, providing and transforming input files before they are processed by Rollup. The first hook of the build phase is [options](guide/en/#options), the last one is always [buildEnd](guide/en/#buildend). + +Additionally, in watch mode the [watchChange](guide/en/#watchchange) hook can be triggered at any time to notify a new run will be triggered once the current run has generated its outputs. Also, when watcher closes, the [closeWatcher](guide/en/#closewatcher) hook will be triggered. See [Output Generation Hooks](guide/en/#output-generation-hooks) for hooks that run during the output generation phase to modify the generated output.