From 7e5c0d29f3edfbf4f8530d3a76f21e14f5dd4552 Mon Sep 17 00:00:00 2001 From: Matt DesLauriers Date: Sun, 10 May 2020 08:21:35 +0100 Subject: [PATCH] Configurable Build Delay (Currently Hardcoded at 200ms) (#3502) * add ability to configure build delay * Change logic to take time diff from configs and add tests * Add documentation Co-authored-by: Lukas Taegert-Atkinson Co-authored-by: Lukas Taegert-Atkinson --- docs/01-command-line-reference.md | 1 + docs/02-javascript-api.md | 1 + docs/999-big-list-of-options.md | 14 +-- src/rollup/types.d.ts | 1 + src/watch/watch.ts | 39 ++++---- test/watch/index.js | 154 +++++++++++++++++++++--------- 6 files changed, 144 insertions(+), 66 deletions(-) diff --git a/docs/01-command-line-reference.md b/docs/01-command-line-reference.md index b2ee7b4619f..7b3ab548363 100755 --- a/docs/01-command-line-reference.md +++ b/docs/01-command-line-reference.md @@ -97,6 +97,7 @@ export default { // can be an array (for multiple inputs) }, watch: { + buildDelay, chokidar, clearScreen, skipWrite, diff --git a/docs/02-javascript-api.md b/docs/02-javascript-api.md index fb45aebc1e8..04920b79e93 100755 --- a/docs/02-javascript-api.md +++ b/docs/02-javascript-api.md @@ -184,6 +184,7 @@ const watchOptions = { ...inputOptions, output: [outputOptions], watch: { + buildDelay, chokidar, clearScreen, skipWrite, diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index 67547c6bbe9..88de0547238 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -1274,6 +1274,12 @@ For each key, the first number represents the elapsed time while the second repr These options only take effect when running Rollup with the `--watch` flag, or using `rollup.watch`. +#### watch.buildDelay +Type: `number`
+Default: `0` + +Configures how long Rollup will wait for further changes until it triggers a rebuild in milliseconds. By default, Rollup does not wait but there is a small debounce timeout configured in the chokidar instance. Setting this to a value greater than `0` will mean that Rollup will only triger a rebuild if there was no change for the configured number of milliseconds. If several configurations are watched, Rollup will use the largest configured build delay. + #### watch.chokidar Type: `ChokidarOptions`
@@ -1281,21 +1287,18 @@ An optional object of watch options that will be passed to the bundled [chokidar #### watch.clearScreen Type: `boolean`
-CLI: `--watch.clearScreen`/`--no-watch.clearScreen`
Default: `true` Whether to clear the screen when a rebuild is triggered. #### watch.skipWrite Type: `boolean`
- Default: `false` Whether to skip the `bundle.write()` step when a rebuild is triggered. #### watch.exclude -Type: `string`
-CLI: `--watch.exclude ` +Type: `string` Prevent files from being watched: @@ -1310,8 +1313,7 @@ export default { ``` #### watch.include -Type: `string`
-CLI: `--watch.include ` +Type: `string` Limit the file-watching to certain files: diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 44442228097..6e80d55a489 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -634,6 +634,7 @@ export interface ChokidarOptions { } export interface WatcherOptions { + buildDelay?: number; chokidar?: ChokidarOptions; clearScreen?: boolean; exclude?: string[]; diff --git a/src/watch/watch.ts b/src/watch/watch.ts index 96b36521467..5141fc330e7 100644 --- a/src/watch/watch.ts +++ b/src/watch/watch.ts @@ -7,17 +7,16 @@ import { RollupBuild, RollupCache, RollupWatcher, - WatcherOptions, + WatcherOptions } from '../rollup/types'; import { mergeOptions } from '../utils/mergeOptions'; import { ensureArray, GenericConfigObject } from '../utils/parseOptions'; import { FileWatcher } from './fileWatcher'; -const DELAY = 200; - export class Watcher { emitter: RollupWatcher; + private buildDelay = 0; private buildTimeout: NodeJS.Timer | null = null; private invalidatedIds: Set = new Set(); private rerun = false; @@ -27,7 +26,15 @@ export class Watcher { constructor(configs: GenericConfigObject[] | GenericConfigObject, emitter: RollupWatcher) { this.emitter = emitter; emitter.close = this.close.bind(this); - this.tasks = ensureArray(configs).map((config) => new Task(this, config)); + const configArray = ensureArray(configs); + this.tasks = configArray.map(config => new Task(this, config)); + this.buildDelay = configArray.reduce( + (buildDelay, { watch }: any) => + watch && typeof watch.buildDelay === 'number' + ? Math.max(buildDelay, (watch as WatcherOptions).buildDelay!) + : buildDelay, + this.buildDelay + ); this.running = true; process.nextTick(() => this.run()); } @@ -63,14 +70,14 @@ export class Watcher { this.invalidatedIds.clear(); this.emit('restart'); this.run(); - }, DELAY); + }, this.buildDelay); } private async run() { this.running = true; this.emit('event', { - code: 'START', + code: 'START' }); try { @@ -79,13 +86,13 @@ export class Watcher { } this.running = false; this.emit('event', { - code: 'END', + code: 'END' }); } catch (error) { this.running = false; this.emit('event', { code: 'ERROR', - error, + error }); } @@ -119,7 +126,7 @@ export class Task { this.skipWrite = config.watch && !!(config.watch as GenericConfigObject).skipWrite; this.options = mergeOptions(config); this.outputs = this.options.output; - this.outputFiles = this.outputs.map((output) => { + this.outputFiles = this.outputs.map(output => { if (output.file || output.dir) return path.resolve(output.file || output.dir!); return undefined as any; }); @@ -129,7 +136,7 @@ export class Task { this.fileWatcher = new FileWatcher(this, { ...watchOptions.chokidar, disableGlobbing: true, - ignoreInitial: true, + ignoreInitial: true }); } @@ -156,7 +163,7 @@ export class Task { const options = { ...this.options, - cache: this.cache, + cache: this.cache }; const start = Date.now(); @@ -164,7 +171,7 @@ export class Task { this.watcher.emit('event', { code: 'BUNDLE_START', input: this.options.input, - output: this.outputFiles, + output: this.outputFiles }); try { @@ -173,13 +180,13 @@ export class Task { return; } this.updateWatchedFiles(result); - this.skipWrite || (await Promise.all(this.outputs.map((output) => result.write(output)))); + this.skipWrite || (await Promise.all(this.outputs.map(output => result.write(output)))); this.watcher.emit('event', { code: 'BUNDLE_END', duration: Date.now() - start, input: this.options.input, output: this.outputFiles, - result, + result }); } catch (error) { if (this.closed) { @@ -192,7 +199,7 @@ export class Task { } } if (error.id) { - this.cache.modules = this.cache.modules.filter((module) => module.id !== error.id); + this.cache.modules = this.cache.modules.filter(module => module.id !== error.id); } throw error; } @@ -222,7 +229,7 @@ export class Task { if (!this.filter(id)) return; this.watched.add(id); - if (this.outputFiles.some((file) => file === id)) { + if (this.outputFiles.some(file => file === id)) { throw new Error('Cannot import the generated bundle'); } diff --git a/test/watch/index.js b/test/watch/index.js index 28454405ae2..e50eef1f6ea 100644 --- a/test/watch/index.js +++ b/test/watch/index.js @@ -66,6 +66,11 @@ describe('rollup.watch', () => { }); } + function getTimeDiffInMs(previous) { + const [seconds, nanoseconds] = process.hrtime(previous); + return seconds * 1e3 + nanoseconds / 1e6; + } + it('watches a file and triggers reruns if necessary', () => { let triggerRestart = false; @@ -491,46 +496,6 @@ describe('rollup.watch', () => { }); }); - it('recovers from an error even when erroring dependency was "renamed" (#38)', () => { - return sander - .copydir('test/watch/samples/dependency') - .to('test/_tmp/input') - .then(() => { - watcher = rollup.watch({ - input: 'test/_tmp/input/main.js', - output: { - file: 'test/_tmp/output/bundle.js', - format: 'cjs' - } - }); - - return sequence(watcher, [ - 'START', - 'BUNDLE_START', - 'BUNDLE_END', - 'END', - () => { - assert.strictEqual(run('../_tmp/output/bundle.js'), 43); - sander.unlinkSync('test/_tmp/input/dep.js'); - sander.writeFileSync('test/_tmp/input/dep.js', 'export nope;'); - }, - 'START', - 'BUNDLE_START', - 'ERROR', - () => { - sander.unlinkSync('test/_tmp/input/dep.js'); - sander.writeFileSync('test/_tmp/input/dep.js', 'export const value = 43;'); - }, - 'START', - 'BUNDLE_START', - 'BUNDLE_END', - 'END', - () => { - assert.strictEqual(run('../_tmp/output/bundle.js'), 44); - } - ]); - }); - }); it('handles closing the watcher during a build', () => { return sander .copydir('test/watch/samples/basic') @@ -1074,6 +1039,110 @@ describe('rollup.watch', () => { }); }); + it('rebuilds immediately by default', async () => { + await sander.copydir('test/watch/samples/basic').to('test/_tmp/input'); + watcher = rollup.watch({ + input: 'test/_tmp/input/main.js', + output: { + file: 'test/_tmp/output/bundle.js', + format: 'cjs' + } + }); + + let startTime; + return sequence(watcher, [ + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + sander.writeFileSync('test/_tmp/input/main.js', 'export default 43;'); + startTime = process.hrtime(); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 43); + const timeDiff = getTimeDiffInMs(startTime); + assert.ok(timeDiff < 400, `Time difference ${timeDiff} < 400`); + } + ]); + }); + + it('observes configured build delays', async () => { + await sander.copydir('test/watch/samples/basic').to('test/_tmp/input'); + watcher = rollup.watch([ + { + input: 'test/_tmp/input/main.js', + output: { + file: 'test/_tmp/output/bundle.js', + format: 'cjs' + } + }, + { + input: 'test/_tmp/input/main.js', + watch: { clearScreen: true }, + output: { + file: 'test/_tmp/output/bundle.js', + format: 'cjs' + } + }, + { + input: 'test/_tmp/input/main.js', + watch: { buildDelay: 1000 }, + output: { + file: 'test/_tmp/output/bundle.js', + format: 'cjs' + } + }, + { + input: 'test/_tmp/input/main.js', + watch: { buildDelay: 50 }, + output: { + file: 'test/_tmp/output/bundle.js', + format: 'cjs' + } + } + ]); + + let startTime; + return sequence(watcher, [ + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'BUNDLE_START', + 'BUNDLE_END', + 'BUNDLE_START', + 'BUNDLE_END', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + sander.writeFileSync('test/_tmp/input/main.js', 'export default 43;'); + startTime = process.hrtime(); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'BUNDLE_START', + 'BUNDLE_END', + 'BUNDLE_START', + 'BUNDLE_END', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 43); + const timeDiff = getTimeDiffInMs(startTime); + assert.ok(timeDiff > 1000, `Time difference ${timeDiff} > 1000`); + } + ]); + }); + describe('addWatchFile', () => { it('supports adding additional watch files in plugin hooks', () => { const watchChangeIds = []; @@ -1256,10 +1325,7 @@ describe('rollup.watch', () => { transform(code, id) { if (id.endsWith('dep1.js')) { this.addWatchFile(path.resolve('test/_tmp/input/dep2.js')); - const text = sander - .readFileSync('test/_tmp/input/dep2.js') - .toString() - .trim(); + const text = sander.readFileSync('test/_tmp/input/dep2.js').toString().trim(); return `export default ${JSON.stringify(text)}`; } }