diff --git a/cli/run/watch-cli.ts b/cli/run/watch-cli.ts index cd845424eff..8ad23b4976a 100644 --- a/cli/run/watch-cli.ts +++ b/cli/run/watch-cli.ts @@ -56,7 +56,7 @@ export async function watch(command: Record): Promise { return; } if (watcher) { - watcher.close(); + await watcher.close(); } start(options, warnings); } catch (err: any) { @@ -136,12 +136,12 @@ export async function watch(command: Record): Promise { }); } - function close(code: number | null): void { + async function close(code: number | null): Promise { process.removeListener('uncaughtException', close); // removing a non-existent listener is a no-op process.stdin.removeListener('end', close); - if (watcher) watcher.close(); + if (watcher) await watcher.close(); if (configWatcher) configWatcher.close(); if (code) { diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index 228b0d81807..89d30b1ea79 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -50,7 +50,7 @@ export default ({ - Plugins should have a clear name with `rollup-plugin-` prefix. - Include `rollup-plugin` keyword in `package.json`. -- Plugins should be tested. We recommend [mocha](https://github.com/mochajs/mocha) or [ava](https://github.com/avajs/ava) which support promises out of the box. +- Plugins should be tested. We recommend [mocha](https://github.com/mochajs/mocha) or [ava](https://github.com/avajs/ava) which support Promises out of the box. - Use asynchronous methods when it is possible. - Document your plugin in English. - Make sure your plugin outputs correct source mappings if appropriate. @@ -68,7 +68,7 @@ The name of the plugin, for use in error messages and warnings. To interact with the build process, your plugin object includes 'hooks'. Hooks are functions which are called at various stages of the build. Hooks can affect how a build is run, provide information about a build, or modify a build once complete. There are different kinds of hooks: -- `async`: The hook may also return a promise resolving to the same type of value; otherwise, the hook is marked as `sync`. +- `async`: The hook may also return a Promise resolving to the same type of value; otherwise, the hook is marked as `sync`. - `first`: If several plugins implement this hook, the hooks are run sequentially until a hook returns a value other than `null` or `undefined`. - `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. @@ -96,9 +96,9 @@ Called on each `rollup.rollup` build. This is the recommended hook to use when y #### `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. +**Type:** `() => void`
**Kind:** `async, parallel`
**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. +Notifies a plugin when the watcher process will close so that all open resources can be closed too. If a Promise is returned, Rollup will wait for the Promise to resolve before closing the process. This hook cannot be used by output plugins. #### `load` @@ -302,9 +302,9 @@ You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfo) to find out the #### `watchChange` -**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). +**Type:** `watchChange: (id: string, change: {event: 'create' | 'update' | 'delete'}) => void`
**Kind:** `async, parallel`
**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. If a Promise is returned, Rollup will wait for the Promise to resolve before scheduling another build. This hook cannot be used by output plugins. The second argument contains additional details of the change event. ### Output Generation Hooks @@ -770,7 +770,7 @@ Loads and parses the module corresponding to the given id, attaching additional This allows you to inspect the final content of modules before deciding how to resolve them in the [`resolveId`](guide/en/#resolveid) hook and e.g. resolve to a proxy module instead. If the module becomes part of the graph later, there is no additional overhead from using this context function as the module will not be parsed again. The signature allows you to directly pass the return value of [`this.resolve`](guide/en/#thisresolve) to this function as long as it is neither `null` nor external. -The returned promise will resolve once the module has been fully transformed and parsed but before any imports have been resolved. That means that the resulting `ModuleInfo` will have empty `importedIds`, `dynamicallyImportedIds`, `importedIdResolutions` and `dynamicallyImportedIdResolutions`. This helps to avoid deadlock situations when awaiting `this.load` in a `resolveId` hook. If you are interested in `importedIds` and `dynamicallyImportedIds`, you can either implement a `moduleParsed` hook or pass the `resolveDependencies` flag, which will make the promise returned by `this.load` wait until all dependency ids have been resolved. +The returned Promise will resolve once the module has been fully transformed and parsed but before any imports have been resolved. That means that the resulting `ModuleInfo` will have empty `importedIds`, `dynamicallyImportedIds`, `importedIdResolutions` and `dynamicallyImportedIdResolutions`. This helps to avoid deadlock situations when awaiting `this.load` in a `resolveId` hook. If you are interested in `importedIds` and `dynamicallyImportedIds`, you can either implement a `moduleParsed` hook or pass the `resolveDependencies` flag, which will make the Promise returned by `this.load` wait until all dependency ids have been resolved. Note that with regard to the `moduleSideEffects`, `syntheticNamedExports` and `meta` options, the same restrictions apply as for the `resolveId` hook: Their values only have an effect if the module has not been loaded yet. Thus, it is very important to use `this.resolve` first to find out if any plugins want to set special values for these options in their `resolveId` hook, and pass these options on to `this.load` if appropriate. The example below showcases how this can be handled to add a proxy module for modules containing a special code comment. Note the special handling for re-exporting the default export: diff --git a/docs/build-hooks.mmd b/docs/build-hooks.mmd index 93da9f1a6b9..889bc5baac4 100644 --- a/docs/build-hooks.mmd +++ b/docs/build-hooks.mmd @@ -32,10 +32,10 @@ flowchart TB transform("transform"):::hook-sequential click transform "/guide/en/#transform" _parent - watchchange("watchChange"):::hook-sequential-sync + watchchange("watchChange"):::hook-parallel click watchchange "/guide/en/#watchchange" _parent - closewatcher("closeWatcher"):::hook-sequential-sync + closewatcher("closeWatcher"):::hook-parallel click closewatcher "/guide/en/#closewatcher" _parent options diff --git a/package.json b/package.json index f4c446ae61b..b2b7a596dc5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "rollup": "dist/bin/rollup" }, "scripts": { - "build": "shx rm -rf dist && git rev-parse HEAD > .commithash && rollup --config rollup.config.ts --configPlugin typescript && shx cp src/rollup/types.d.ts dist/rollup.d.ts && shx chmod a+x dist/bin/rollup", + "build": "shx rm -rf dist && node scripts/update-git-commit.js && rollup --config rollup.config.ts --configPlugin typescript && shx cp src/rollup/types.d.ts dist/rollup.d.ts && shx chmod a+x dist/bin/rollup", "build:cjs": "shx rm -rf dist && rollup --config rollup.config.ts --configPlugin typescript --configTest && shx cp src/rollup/types.d.ts dist/rollup.d.ts && shx chmod a+x dist/bin/rollup", "build:bootstrap": "node dist/bin/rollup --config rollup.config.ts --configPlugin typescript && shx cp src/rollup/types.d.ts dist/rollup.d.ts && shx chmod a+x dist/bin/rollup", "ci:lint": "npm run lint:nofix", @@ -22,10 +22,9 @@ "perf": "npm run build:cjs && node --expose-gc scripts/perf.js", "perf:debug": "node --inspect-brk scripts/perf-debug.js", "perf:init": "node scripts/perf-init.js", - "postinstall": "husky install", - "postpublish": "pinst --enable && git push && git push --tags", + "postpublish": "git push && git push --tags", "prepare": "husky install && npm run build", - "prepublishOnly": "pinst --disable && npm ci && npm run lint:nofix && npm run security && npm run build:bootstrap && npm run test:all", + "prepublishOnly": "npm ci && npm run lint:nofix && npm run security && npm run build:bootstrap && npm run test:all", "security": "npm audit", "test": "npm run build && npm run test:all", "test:cjs": "npm run build:cjs && npm run test:only", @@ -97,7 +96,6 @@ "magic-string": "^0.25.7", "mocha": "^9.2.1", "nyc": "^15.1.0", - "pinst": "^3.0.0", "prettier": "^2.5.1", "pretty-bytes": "^5.6.0", "pretty-ms": "^7.0.1", diff --git a/scripts/update-git-commit.js b/scripts/update-git-commit.js new file mode 100644 index 00000000000..2f167f35d27 --- /dev/null +++ b/scripts/update-git-commit.js @@ -0,0 +1,12 @@ +const { execSync } = require('child_process'); +const { writeFileSync } = require('fs'); +const { join } = require('path'); + +let revision; +try { + revision = execSync('git rev-parse HEAD').toString().trim(); +} catch (e) { + console.warn('Could not determine git commit when building Rollup.'); + revision = '(could not be determined)'; +} +writeFileSync(join(__dirname, '../.commithash'), revision); diff --git a/src/Graph.ts b/src/Graph.ts index ee6fb26951b..2bc50b79764 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -78,15 +78,11 @@ export default class Graph { if (watcher) { this.watchMode = true; - const handleChange: WatchChangeHook = (...args) => - this.pluginDriver.hookSeqSync('watchChange', args); - 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); - }); + const handleChange = (...args: Parameters) => + this.pluginDriver.hookParallel('watchChange', args); + const handleClose = () => this.pluginDriver.hookParallel('closeWatcher', []); + watcher.onCurrentAwaited('change', handleChange); + watcher.onCurrentAwaited('close', handleClose); } this.pluginDriver = new PluginDriver(this, options, options.plugins, this.pluginCache); this.acornParser = acorn.Parser.extend(...(options.acornInjectPlugins as any)); diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 55da5d5f34c..6144a7a1c31 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -344,7 +344,7 @@ export type WatchChangeHook = ( this: PluginContext, id: string, change: { event: ChangeEvent } -) => void; +) => Promise | void; /** * use this type for plugin annotation @@ -375,7 +375,7 @@ export interface PluginHooks extends OutputPluginHooks { buildEnd: (this: PluginContext, err?: Error) => Promise | void; buildStart: (this: PluginContext, options: NormalizedInputOptions) => Promise | void; closeBundle: (this: PluginContext) => Promise | void; - closeWatcher: (this: PluginContext) => void; + closeWatcher: (this: PluginContext) => Promise | void; load: LoadHook; moduleParsed: ModuleParsedHook; options: ( @@ -440,7 +440,9 @@ export type AsyncPluginHooks = | 'shouldTransformCachedModule' | 'transform' | 'writeBundle' - | 'closeBundle'; + | 'closeBundle' + | 'closeWatcher' + | 'watchChange'; export type PluginValueHooks = 'banner' | 'footer' | 'intro' | 'outro'; @@ -458,13 +460,11 @@ export type FirstPluginHooks = export type SequentialPluginHooks = | 'augmentChunkHash' - | 'closeWatcher' | 'generateBundle' | 'options' | 'outputOptions' | 'renderChunk' - | 'transform' - | 'watchChange'; + | 'transform'; export type ParallelPluginHooks = | 'banner' @@ -477,7 +477,9 @@ export type ParallelPluginHooks = | 'renderError' | 'renderStart' | 'writeBundle' - | 'closeBundle'; + | 'closeBundle' + | 'closeWatcher' + | 'watchChange'; interface OutputPluginValueHooks { banner: AddonHook; @@ -898,6 +900,24 @@ interface TypedEventEmitter any } setMaxListeners(n: number): this; } +export interface RollupAwaitingEmitter any }> + extends TypedEventEmitter { + close(): Promise; + emitAndAwait(event: K, ...args: Parameters): Promise[]>; + /** + * Registers an event listener that will be awaited before Rollup continues + * for events emitted via emitAndAwait. All listeners will be awaited in + * parallel while rejections are tracked via Promise.all. + * Listeners are removed automatically when removeAwaited is called, which + * happens automatically after each run. + */ + onCurrentAwaited( + event: K, + listener: (...args: Parameters) => Promise> + ): this; + removeAwaited(): this; +} + export type RollupWatcherEvent = | { code: 'START' } | { code: 'BUNDLE_START'; input?: InputOption; output: readonly string[] } @@ -911,15 +931,12 @@ export type RollupWatcherEvent = | { code: 'END' } | { code: 'ERROR'; error: RollupError; result: RollupBuild | null }; -export interface RollupWatcher - extends TypedEventEmitter<{ - change: (id: string, change: { event: ChangeEvent }) => void; - close: () => void; - event: (event: RollupWatcherEvent) => void; - restart: () => void; - }> { - close(): void; -} +export type RollupWatcher = RollupAwaitingEmitter<{ + change: (id: string, change: { event: ChangeEvent }) => void; + close: () => void; + event: (event: RollupWatcherEvent) => void; + restart: () => void; +}>; export function watch(config: RollupWatchOptions | RollupWatchOptions[]): RollupWatcher; diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index 512120459e2..d4e8de71dfa 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -270,17 +270,6 @@ export class PluginDriver { return promise; } - // chains synchronously, ignores returns - hookSeqSync( - hookName: H, - args: Parameters, - replaceContext?: ReplaceContext - ): void { - for (const plugin of this.plugins) { - this.runHookSync(hookName, args, plugin, replaceContext); - } - } - /** * Run an async plugin hook and return the result. * @param hookName Name of the plugin hook. Must be either in `PluginHooks` or `OutputPluginValueHooks`. diff --git a/src/watch/WatchEmitter.ts b/src/watch/WatchEmitter.ts new file mode 100644 index 00000000000..edc68f85471 --- /dev/null +++ b/src/watch/WatchEmitter.ts @@ -0,0 +1,48 @@ +import { EventEmitter } from 'events'; + +type PromiseReturn any> = ( + ...args: Parameters +) => Promise>; + +export class WatchEmitter< + T extends { [event: string]: (...args: any) => any } +> extends EventEmitter { + private awaitedHandlers: { + [K in keyof T]?: PromiseReturn[]; + } = Object.create(null); + + constructor() { + super(); + // Allows more than 10 bundles to be watched without + // showing the `MaxListenersExceededWarning` to the user. + this.setMaxListeners(Infinity); + } + + // Will be overwritten by Rollup + async close(): Promise {} + + emitAndAwait( + event: K, + ...args: Parameters + ): Promise[]> { + this.emit(event as string, ...(args as any[])); + return Promise.all(this.getHandlers(event).map(handler => handler(...args))); + } + + onCurrentAwaited( + event: K, + listener: (...args: Parameters) => Promise> + ): this { + this.getHandlers(event).push(listener); + return this; + } + + removeAwaited(): this { + this.awaitedHandlers = {}; + return this; + } + + private getHandlers(event: K): PromiseReturn[] { + return this.awaitedHandlers[event] || (this.awaitedHandlers[event] = []); + } +} diff --git a/src/watch/watch-proxy.ts b/src/watch/watch-proxy.ts index 0ca30d39a1b..b7371b0398a 100644 --- a/src/watch/watch-proxy.ts +++ b/src/watch/watch-proxy.ts @@ -1,21 +1,10 @@ -import { EventEmitter } from 'events'; import type { RollupWatcher } from '../rollup/types'; import { ensureArray } from '../utils/ensureArray'; import { errInvalidOption, error } from '../utils/error'; import type { GenericConfigObject } from '../utils/options/options'; +import { WatchEmitter } from './WatchEmitter'; import { loadFsEvents } from './fsevents-importer'; -class WatchEmitter extends EventEmitter { - constructor() { - super(); - // Allows more than 10 bundles to be watched without - // showing the `MaxListenersExceededWarning` to the user. - this.setMaxListeners(Infinity); - } - - close() {} -} - export default function watch(configs: GenericConfigObject[] | GenericConfigObject): RollupWatcher { const emitter = new WatchEmitter() as RollupWatcher; const configArray = ensureArray(configs); diff --git a/src/watch/watch.ts b/src/watch/watch.ts index 2c2e16c0675..2635a3a4f7b 100644 --- a/src/watch/watch.ts +++ b/src/watch/watch.ts @@ -57,12 +57,12 @@ export class Watcher { process.nextTick(() => this.run()); } - close(): void { + async close(): Promise { if (this.buildTimeout) clearTimeout(this.buildTimeout); for (const task of this.tasks) { task.close(); } - this.emitter.emit('close'); + await this.emitter.emitAndAwait('close'); this.emitter.removeAllListeners(); } @@ -87,14 +87,29 @@ export class Watcher { if (this.buildTimeout) clearTimeout(this.buildTimeout); - this.buildTimeout = setTimeout(() => { + this.buildTimeout = setTimeout(async () => { this.buildTimeout = null; - for (const [id, event] of this.invalidatedIds) { - this.emitter.emit('change', id, { event }); + try { + await Promise.all( + [...this.invalidatedIds].map(([id, event]) => + this.emitter.emitAndAwait('change', id, { event }) + ) + ); + this.invalidatedIds.clear(); + this.emitter.emit('restart'); + this.emitter.removeAwaited(); + this.run(); + } catch (error: any) { + this.invalidatedIds.clear(); + this.emitter.emit('event', { + code: 'ERROR', + error, + result: null + }); + this.emitter.emit('event', { + code: 'END' + }); } - this.invalidatedIds.clear(); - this.emitter.emit('restart'); - this.run(); }, this.buildDelay); } diff --git a/test/watch/index.js b/test/watch/index.js index d759e0c6c19..b184b621df0 100644 --- a/test/watch/index.js +++ b/test/watch/index.js @@ -43,11 +43,20 @@ describe('rollup.watch', () => { watcher.close(); fulfil(); } else if (typeof next === 'string') { + const [eventCode, eventMessage] = next.split(':'); watcher.once('event', event => { - if (event.code !== next) { + if (event.code !== eventCode) { watcher.close(); if (event.code === 'ERROR') console.log(event.error); - reject(new Error(`Expected ${next} event, got ${event.code}`)); + reject(new Error(`Expected ${eventCode} event, got ${event.code}`)); + } else if ( + eventCode === 'ERROR' && + eventMessage && + event.error.message !== eventMessage + ) { + reject( + new Error(`Expected to throw "${eventMessage}" but got "${event.error.message}".`) + ); } else { go(event); } @@ -514,7 +523,8 @@ describe('rollup.watch', () => { }, 'START', 'BUNDLE_START', - 'ERROR', + 'ERROR:Unexpected token', + 'END', () => { atomicWriteFileSync('test/_tmp/input/main.js', 'export default 43;'); }, @@ -541,7 +551,8 @@ describe('rollup.watch', () => { return sequence(watcher, [ 'START', 'BUNDLE_START', - 'ERROR', + 'ERROR:Unexpected token', + 'END', () => { assert.strictEqual(existsSync('../_tmp/output/bundle.js'), false); atomicWriteFileSync('test/_tmp/input/main.js', 'export default 43;'); @@ -577,7 +588,8 @@ describe('rollup.watch', () => { return sequence(watcher, [ 'START', 'BUNDLE_START', - 'ERROR', + 'ERROR:The first run failed, try again.', + 'END', () => { assert.strictEqual(existsSync('../_tmp/output/bundle.js'), false); atomicWriteFileSync('test/_tmp/input/main.js', 'export default 43;'); @@ -592,6 +604,50 @@ describe('rollup.watch', () => { ]); }); + it('awaits and recovers from a plugin error in the watchChange hook', async () => { + let fail = true; + await copy('test/watch/samples/basic', 'test/_tmp/input'); + watcher = rollup.watch({ + input: 'test/_tmp/input/main.js', + plugins: { + async watchChange() { + await new Promise(resolve => setTimeout(resolve, 300)); + if (fail) { + this.error('Failed in watchChange'); + } + } + }, + output: { + file: 'test/_tmp/output/bundle.js', + format: 'cjs', + exports: 'auto' + } + }); + return sequence(watcher, [ + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + atomicWriteFileSync('test/_tmp/input/main.js', 'export default 21;'); + }, + 'ERROR:Failed in watchChange', + 'END', + () => { + fail = false; + atomicWriteFileSync('test/_tmp/input/main.js', 'export default 43;'); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 43); + } + ]); + }); + it('recovers from an error even when erroring entry was "renamed" (#38)', async () => { await copy('test/watch/samples/basic', 'test/_tmp/input'); watcher = rollup.watch({ @@ -614,7 +670,8 @@ describe('rollup.watch', () => { }, 'START', 'BUNDLE_START', - 'ERROR', + 'ERROR:Unexpected token', + 'END', () => { unlinkSync('test/_tmp/input/main.js'); atomicWriteFileSync('test/_tmp/input/main.js', 'export default 43;'); @@ -651,7 +708,8 @@ describe('rollup.watch', () => { }, 'START', 'BUNDLE_START', - 'ERROR', + 'ERROR:Unexpected token', + 'END', () => { unlinkSync('test/_tmp/input/dep.js'); atomicWriteFileSync('test/_tmp/input/dep.js', 'export const value = 43;'); @@ -779,9 +837,9 @@ describe('rollup.watch', () => { }, 'START', 'BUNDLE_START', - 'ERROR', - event => { - assert.strictEqual(event.error.message, 'Cannot import the generated bundle'); + 'ERROR:Cannot import the generated bundle', + 'END', + () => { atomicWriteFileSync('test/_tmp/input/main.js', 'export default 43;'); }, 'START', @@ -1087,56 +1145,56 @@ describe('rollup.watch', () => { ]); }); - it('runs transforms again on previously erroring files that were changed back', () => { + it('runs transforms again on previously erroring files that were changed back', async () => { const brokenFiles = new Set(); + await copy('test/watch/samples/basic', 'test/_tmp/input'); const INITIAL_CONTENT = 'export default 42;'; - fs.writeFile('test/_tmp/input/main.js', INITIAL_CONTENT).then(() => { - watcher = rollup.watch({ - input: 'test/_tmp/input/main.js', - plugins: { - transform(code, id) { - if (code.includes('broken')) { - brokenFiles.add(id); - throw new Error('Broken in transform'); - } - brokenFiles.delete(id); - }, - generateBundle() { - if (brokenFiles.size > 0) { - throw new Error('Broken in generate'); - } + atomicWriteFileSync('test/_tmp/input/main.js', INITIAL_CONTENT); + watcher = rollup.watch({ + input: 'test/_tmp/input/main.js', + plugins: { + transform(code, id) { + if (code.includes('broken')) { + brokenFiles.add(id); + throw new Error('Broken in transform'); } + brokenFiles.delete(id); }, - output: { - file: 'test/_tmp/output/bundle.js', - format: 'cjs', - exports: 'auto' - } - }); - return sequence(watcher, [ - 'START', - 'BUNDLE_START', - 'BUNDLE_END', - 'END', - () => { - assert.strictEqual(run('../_tmp/output/bundle.js'), 42); - atomicWriteFileSync('test/_tmp/input/main.js', 'export default "broken";'); - }, - 'START', - 'BUNDLE_START', - 'ERROR', - () => { - atomicWriteFileSync('test/_tmp/input/main.js', INITIAL_CONTENT); - }, - 'START', - 'BUNDLE_START', - 'BUNDLE_END', - 'END', - () => { - assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + generateBundle() { + if (brokenFiles.size > 0) { + throw new Error('Broken in generate'); + } } - ]); + }, + output: { + file: 'test/_tmp/output/bundle.js', + format: 'cjs', + exports: 'auto' + } }); + return sequence(watcher, [ + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + atomicWriteFileSync('test/_tmp/input/main.js', 'export default "broken";'); + }, + 'START', + 'BUNDLE_START', + 'ERROR:Broken in transform', + () => { + atomicWriteFileSync('test/_tmp/input/main.js', INITIAL_CONTENT); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.strictEqual(run('../_tmp/output/bundle.js'), 42); + } + ]); }); it('skips filesystem writes when configured', async () => {