Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved watcher hooks #3841

Merged
merged 8 commits into from Oct 31, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 14 additions & 1 deletion docs/05-plugin-development.md
Expand Up @@ -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`<br>
Kind: `sync, sequential`<br>
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}`<br>
Kind: `async, first`<br>
Expand Down Expand Up @@ -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`<br>
Type: `(id: string, isDeleted: boolean) => void`<br>
Amareis marked this conversation as resolved.
Show resolved Hide resolved
Kind: `sync, sequential`<br>
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

Expand Down Expand Up @@ -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.
Amareis marked this conversation as resolved.
Show resolved Hide resolved

#### `this.meta: {rollupVersion: string, watchMode: boolean}`

An object containing potentially useful Rollup metadata:
Expand Down
5 changes: 4 additions & 1 deletion src/Graph.ts
Expand Up @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions src/rollup/types.d.ts
Expand Up @@ -195,6 +195,7 @@ export interface PluginContext extends MinimalPluginContext {
getFileName: (fileReferenceId: string) => string;
getModuleIds: () => IterableIterator<string>;
getModuleInfo: GetModuleInfo;
getWatchFiles: () => string[];
/** @deprecated Use `this.resolve` instead */
isExternal: IsExternal;
/** @deprecated Use `this.getModuleIds` instead */
Expand Down Expand Up @@ -345,13 +346,14 @@ export interface OutputBundleWithPlaceholders {
export interface PluginHooks extends OutputPluginHooks {
buildEnd: (this: PluginContext, err?: Error) => Promise<void> | void;
buildStart: (this: PluginContext, options: NormalizedInputOptions) => Promise<void> | 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 {
Expand Down Expand Up @@ -419,6 +421,7 @@ export type FirstPluginHooks =

export type SequentialPluginHooks =
| 'augmentChunkHash'
| 'closeWatcher'
| 'generateBundle'
| 'options'
| 'outputOptions'
Expand Down Expand Up @@ -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;
}> {
Expand Down
1 change: 1 addition & 0 deletions src/utils/PluginContext.ts
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions src/utils/PluginDriver.ts
Expand Up @@ -47,6 +47,7 @@ const inputHookNames: {
} = {
buildEnd: 1,
buildStart: 1,
closeWatcher: 1,
load: 1,
moduleParsed: 1,
options: 1,
Expand Down
6 changes: 3 additions & 3 deletions src/watch/fileWatcher.ts
Expand Up @@ -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,
Expand All @@ -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;
}
}
1 change: 1 addition & 0 deletions src/watch/fsevents-importer.ts
Expand Up @@ -2,6 +2,7 @@ let fsEvents: any;
let fsEventsImportError: any;

export function loadFsEvents() {
// @ts-ignore
Amareis marked this conversation as resolved.
Show resolved Hide resolved
return import('fsevents')
.then(namespace => {
fsEvents = namespace.default;
Expand Down
19 changes: 13 additions & 6 deletions src/watch/watch.ts
Expand Up @@ -18,6 +18,7 @@ export class Watcher {

private buildDelay = 0;
private buildTimeout: NodeJS.Timer | null = null;
private deletedIds: Set<string> = new Set();
private invalidatedIds: Set<string> = new Set();
private rerun = false;
private running: boolean;
Expand All @@ -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) {
lukastaegert marked this conversation as resolved.
Show resolved Hide resolved
this.emitter.emit(event as any, value);
emit(event: string, ...args: unknown[]) {
this.emitter.emit(event as any, ...args);
Amareis marked this conversation as resolved.
Show resolved Hide resolved
}

invalidate(id?: string) {
invalidate(id?: string, isDeleted?: boolean) {
Amareis marked this conversation as resolved.
Show resolved Hide resolved
if (id) {
this.invalidatedIds.add(id);
if (isDeleted)
this.deletedIds.add(id);
else
this.deletedIds.delete(id)
}
if (this.running) {
this.rerun = true;
Expand All @@ -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));
Amareis marked this conversation as resolved.
Show resolved Hide resolved
}
this.invalidatedIds.clear();
this.deletedIds.clear();
this.emit('restart');
this.run();
}, this.buildDelay);
Expand Down Expand Up @@ -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) {
Expand All @@ -153,7 +160,7 @@ export class Task {
module.originalCode = null as any;
}
}
this.watcher.invalidate(id);
this.watcher.invalidate(id, isDeleted);
}

async run() {
Expand Down
128 changes: 128 additions & 0 deletions test/watch/index.js
Expand Up @@ -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')
Expand Down