From fc2b77315887a4a0dee42076cc26ca29bac42014 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Jul 2017 08:16:56 -0400 Subject: [PATCH 1/8] move build/watch out into separate modules --- bin/src/index.js | 4 +- bin/src/run/build.js | 49 +++++++++++++ bin/src/{runRollup.js => run/index.js} | 98 ++------------------------ bin/src/run/watch.js | 49 +++++++++++++ 4 files changed, 105 insertions(+), 95 deletions(-) create mode 100644 bin/src/run/build.js rename bin/src/{runRollup.js => run/index.js} (65%) create mode 100644 bin/src/run/watch.js diff --git a/bin/src/index.js b/bin/src/index.js index cbc81238cfd..db432c28ce8 100644 --- a/bin/src/index.js +++ b/bin/src/index.js @@ -1,7 +1,7 @@ import minimist from 'minimist'; import help from './help.md'; import { version } from '../../package.json'; -import runRollup from './runRollup'; +import run from './run/index.js'; const command = minimist( process.argv.slice( 2 ), { alias: { @@ -35,5 +35,5 @@ else if ( command.version ) { } else { - runRollup( command ); + run( command ); } diff --git a/bin/src/run/build.js b/bin/src/run/build.js new file mode 100644 index 00000000000..704711c14db --- /dev/null +++ b/bin/src/run/build.js @@ -0,0 +1,49 @@ +import * as rollup from 'rollup'; +import { handleError } from '../logging.js'; +import SOURCEMAPPING_URL from '../sourceMappingUrl.js'; + +export default function build ( options ) { + return rollup.rollup( options ) + .then( bundle => { + if ( options.dest ) { + return bundle.write( options ); + } + + if ( options.targets ) { + let result = null; + + options.targets.forEach( target => { + result = bundle.write( assign( clone( options ), target ) ); + }); + + return result; + } + + if ( options.sourceMap && options.sourceMap !== 'inline' ) { + handleError({ + code: 'MISSING_OUTPUT_OPTION', + message: 'You must specify an --output (-o) option when creating a file with a sourcemap' + }); + } + + return bundle.generate(options).then( ({ code, map }) => { + if ( options.sourceMap === 'inline' ) { + code += `\n//# ${SOURCEMAPPING_URL}=${map.toUrl()}\n`; + } + + process.stdout.write( code ); + }); + }) + .catch( handleError ); +} + +function clone ( object ) { + return assign( {}, object ); +} + +function assign ( target, source ) { + Object.keys( source ).forEach( key => { + target[ key ] = source[ key ]; + }); + return target; +} \ No newline at end of file diff --git a/bin/src/runRollup.js b/bin/src/run/index.js similarity index 65% rename from bin/src/runRollup.js rename to bin/src/run/index.js index 4b6f6a4c134..288316da238 100644 --- a/bin/src/runRollup.js +++ b/bin/src/run/index.js @@ -2,9 +2,9 @@ import path from 'path'; import { realpathSync } from 'fs'; import * as rollup from 'rollup'; import relative from 'require-relative'; -import chalk from 'chalk'; -import { handleWarning, handleError, stderr } from './logging.js'; -import SOURCEMAPPING_URL from './sourceMappingUrl.js'; +import { handleWarning, handleError } from '../logging.js'; +import build from './build.js'; +import watch from './watch.js'; import { install as installSourcemapSupport } from 'source-map-support'; installSourcemapSupport(); @@ -192,96 +192,8 @@ function execute ( options, command ) { }); if ( command.watch ) { - if ( !options.entry || ( !options.dest && !options.targets ) ) { - handleError({ - code: 'WATCHER_MISSING_INPUT_OR_OUTPUT', - message: 'must specify --input and --output when using rollup --watch' - }); - } - - try { - const watch = relative( 'rollup-watch', process.cwd() ); - const watcher = watch( rollup, options ); - - watcher.on( 'event', event => { - switch ( event.code ) { - case 'STARTING': // TODO this isn't emitted by newer versions of rollup-watch - stderr( 'checking rollup-watch version...' ); - break; - - case 'BUILD_START': - stderr( 'bundling...' ); - break; - - case 'BUILD_END': - stderr( 'bundled in ' + event.duration + 'ms. Watching for changes...' ); - break; - - case 'ERROR': - handleError( event.error, true ); - break; - - default: - stderr( 'unknown event', event ); - } - }); - } catch ( err ) { - if ( err.code === 'MODULE_NOT_FOUND' ) { - handleError({ - code: 'ROLLUP_WATCH_NOT_INSTALLED', - message: 'rollup --watch depends on the rollup-watch package, which could not be found. Install it with npm install -D rollup-watch' - }); - } - - handleError( err ); - } + watch( options ); } else { - bundle( options ).catch( handleError ); + build( options ).catch( handleError ); } } - -function clone ( object ) { - return assign( {}, object ); -} - -function assign ( target, source ) { - Object.keys( source ).forEach( key => { - target[ key ] = source[ key ]; - }); - return target; -} - -function bundle ( options ) { - return rollup.rollup( options ) - .then( bundle => { - if ( options.dest ) { - return bundle.write( options ); - } - - if ( options.targets ) { - let result = null; - - options.targets.forEach( target => { - result = bundle.write( assign( clone( options ), target ) ); - }); - - return result; - } - - if ( options.sourceMap && options.sourceMap !== 'inline' ) { - handleError({ - code: 'MISSING_OUTPUT_OPTION', - message: 'You must specify an --output (-o) option when creating a file with a sourcemap' - }); - } - - return bundle.generate(options).then( ({ code, map }) => { - if ( options.sourceMap === 'inline' ) { - code += `\n//# ${SOURCEMAPPING_URL}=${map.toUrl()}\n`; - } - - process.stdout.write( code ); - }); - }) - .catch( handleError ); -} diff --git a/bin/src/run/watch.js b/bin/src/run/watch.js new file mode 100644 index 00000000000..783c8aba168 --- /dev/null +++ b/bin/src/run/watch.js @@ -0,0 +1,49 @@ +import * as rollup from 'rollup'; +import relative from 'require-relative'; +import { handleError, stderr } from '../logging.js'; + +export default function watch ( options ) { + if ( !options.entry || ( !options.dest && !options.targets ) ) { + handleError({ + code: 'WATCHER_MISSING_INPUT_OR_OUTPUT', + message: 'must specify --input and --output when using rollup --watch' + }); + } + + try { + const watch = relative( 'rollup-watch', process.cwd() ); + const watcher = watch( rollup, options ); + + watcher.on( 'event', event => { + switch ( event.code ) { + case 'STARTING': // TODO this isn't emitted by newer versions of rollup-watch + stderr( 'checking rollup-watch version...' ); + break; + + case 'BUILD_START': + stderr( 'bundling...' ); + break; + + case 'BUILD_END': + stderr( 'bundled in ' + event.duration + 'ms. Watching for changes...' ); + break; + + case 'ERROR': + handleError( event.error, true ); + break; + + default: + stderr( 'unknown event', event ); + } + }); + } catch ( err ) { + if ( err.code === 'MODULE_NOT_FOUND' ) { + handleError({ + code: 'ROLLUP_WATCH_NOT_INSTALLED', + message: 'rollup --watch depends on the rollup-watch package, which could not be found. Install it with npm install -D rollup-watch' + }); + } + + handleError( err ); + } +} \ No newline at end of file From e7dbc60119361eb61b44337d9ab06da32fb4f1b9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Jul 2017 11:53:23 -0400 Subject: [PATCH 2/8] coalesce warnings in CLI --- bin/src/run/batchWarnings.js | 210 ++++++++++++++++++++++++++++++ bin/src/run/build.js | 9 ++ bin/src/run/index.js | 20 +-- src/Bundle.js | 2 + src/Module.js | 37 ++---- src/ast/nodes/MemberExpression.js | 3 + src/rollup.js | 2 +- 7 files changed, 245 insertions(+), 38 deletions(-) create mode 100644 bin/src/run/batchWarnings.js diff --git a/bin/src/run/batchWarnings.js b/bin/src/run/batchWarnings.js new file mode 100644 index 00000000000..86092cdab24 --- /dev/null +++ b/bin/src/run/batchWarnings.js @@ -0,0 +1,210 @@ +import chalk from 'chalk'; +import { handleWarning } from '../logging.js'; +import relativeId from '../../../src/utils/relativeId.js'; + +export default function batchWarnings () { + const allWarnings = new Map(); + let count = 0; + + return { + add: warning => { + if ( typeof warning === 'string' ) { + warning = { code: 'UNKNOWN', message: warning }; + } + + if ( !allWarnings.has( warning.code ) ) allWarnings.set( warning.code, [] ); + allWarnings.get( warning.code ).push( warning ); + + count += 1; + }, + + flush: () => { + if ( count === 0 ) return; + + const codes = Array.from( allWarnings.keys() ) + .sort( ( a, b ) => { + if ( handlers[a] && handlers[b] ) { + return handlers[a].priority - handlers[b].priority; + } + + if ( handlers[a] ) return -1; + if ( handlers[b] ) return 1; + return allWarnings.get( b ).length - allWarnings.get( a ).length; + }); + + codes.forEach( code => { + const handler = handlers[ code ]; + const warnings = allWarnings.get( code ); + + if ( handler ) { + handler.fn( warnings ); + } else { + warnings.forEach( warning => { + handleWarning( warning ); + }); + } + }); + } + }; +} + +// TODO select sensible priorities +const handlers = { + UNUSED_EXTERNAL_IMPORT: { + priority: 1, + fn: warnings => { + group( 'Unused external imports' ); + warnings.forEach( warning => { + log( `${warning.message}` ); + }); + } + }, + + UNRESOLVED_IMPORT: { + priority: 1, + fn: warnings => { + group( 'Unresolved dependencies' ); + info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency' ); + + const dependencies = new Map(); + warnings.forEach( warning => { + if ( !dependencies.has( warning.source ) ) dependencies.set( warning.source, [] ); + dependencies.get( warning.source ).push( warning.importer ); + }); + + Array.from( dependencies.keys() ).forEach( dependency => { + const importers = dependencies.get( dependency ); + log( `${chalk.bold( dependency )} (imported by ${importers.join( ', ' )})` ); + }); + } + }, + + MISSING_EXPORT: { + priority: 1, + fn: warnings => { + group( 'Missing exports' ); + info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module' ); + + warnings.forEach( warning => { + log( chalk.bold( warning.importer ) ); + log( `${warning.missing} is not exported by ${warning.exporter}` ); + log( chalk.grey( warning.frame ) ); + }); + } + }, + + THIS_IS_UNDEFINED: { + priority: 1, + fn: warnings => { + group( '`this` has been rewritten to `undefined`' ); + info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined' ); + + const modules = new Map(); + warnings.forEach( warning => { + if ( !modules.has( warning.loc.file ) ) modules.set( warning.loc.file, [] ); + modules.get( warning.loc.file ).push( warning ); + }); + + const allIds = Array.from( modules.keys() ); + const ids = allIds.length > 5 ? allIds.slice( 0, 3 ) : allIds; + + ids.forEach( id => { + const allOccurrences = modules.get( id ); + const occurrences = allOccurrences.length > 5 ? allOccurrences.slice( 0, 3 ) : allOccurrences; + + log( chalk.bold( relativeId( id ) ) ); + log( chalk.grey( occurrences.map( warning => warning.frame ).join( '\n\n' ) ) ); + + if ( allOccurrences.length > occurrences.length ) { + log( `\n...and ${allOccurrences.length - occurrences.length} occurrences` ); + } + }); + + if ( allIds.length > ids.length ) { + log( `\n...and ${allIds.length - ids.length} files` ); + } + } + }, + + EVAL: { + priority: 1, + fn: warnings => { + + } + }, + + NON_EXISTENT_EXPORT: { + priority: 1, + fn: warnings => { + + } + }, + + NAMESPACE_CONFLICT: { + priority: 1, + fn: warnings => { + + } + }, + + DEPRECATED_ES6: { + priority: 1, + fn: warnings => { + + } + }, + + EMPTY_BUNDLE: { + priority: 1, + fn: warnings => { + + } + }, + + MISSING_GLOBAL_NAME: { + priority: 1, + fn: warnings => { + + } + }, + + MISSING_NODE_BUILTINS: { + priority: 1, + fn: warnings => { + + } + }, + + MISSING_FORMAT: { + priority: 1, + fn: warnings => { + + } + }, // TODO make this an error + + SOURCEMAP_BROKEN: { + priority: 1, + fn: warnings => { + + } + }, + + MIXED_EXPORTS: { + priority: 1, + fn: warnings => { + + } + } +}; + +function group ( title ) { + log( `\n${chalk.bold.yellow('(!)')} ${chalk.bold.yellow( title )}` ); +} + +function info ( url ) { + log( chalk.grey( url ) ); +} + +function log ( message ) { + console.warn( message ); // eslint-disable-line no-console +} \ No newline at end of file diff --git a/bin/src/run/build.js b/bin/src/run/build.js index 704711c14db..20ec941718d 100644 --- a/bin/src/run/build.js +++ b/bin/src/run/build.js @@ -1,8 +1,16 @@ import * as rollup from 'rollup'; import { handleError } from '../logging.js'; import SOURCEMAPPING_URL from '../sourceMappingUrl.js'; +import batchWarnings from './batchWarnings.js'; export default function build ( options ) { + let batch; + + if ( !options.onwarn ) { + batch = batchWarnings(); + options.onwarn = batch.add; + } + return rollup.rollup( options ) .then( bundle => { if ( options.dest ) { @@ -34,6 +42,7 @@ export default function build ( options ) { process.stdout.write( code ); }); }) + .then( batch.flush ) .catch( handleError ); } diff --git a/bin/src/run/index.js b/bin/src/run/index.js index 288316da238..54854fca739 100644 --- a/bin/src/run/index.js +++ b/bin/src/run/index.js @@ -3,6 +3,7 @@ import { realpathSync } from 'fs'; import * as rollup from 'rollup'; import relative from 'require-relative'; import { handleWarning, handleError } from '../logging.js'; +import batchWarnings from './batchWarnings.js'; import build from './build.js'; import watch from './watch.js'; @@ -65,14 +66,18 @@ export default function runRollup ( command ) { config = realpathSync( config ); } + const batch = batchWarnings(); + rollup.rollup({ entry: config, external: id => { return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5,id.length) === '.json'; }, - onwarn: handleWarning + onwarn: batch.add }) .then( bundle => { + batch.flush(); + return bundle.generate({ format: 'cjs' }); @@ -169,19 +174,6 @@ function execute ( options, command ) { options.onwarn = () => {}; } - if ( !options.onwarn ) { - const seen = new Set(); - - options.onwarn = warning => { - const str = warning.toString(); - - if ( seen.has( str ) ) return; - seen.add( str ); - - handleWarning( warning ); - }; - } - options.external = external; // Use any options passed through the CLI as overrides. diff --git a/src/Bundle.js b/src/Bundle.js index 884eae76eac..d6e9ace3b04 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -389,6 +389,8 @@ export default class Bundle { this.warn({ code: 'UNRESOLVED_IMPORT', + source, + importer: relativeId( module.id ), message: `'${source}' is imported by ${relativeId( module.id )}, but could not be resolved – treating it as an external dependency`, url: 'https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency' }); diff --git a/src/Module.js b/src/Module.js index bb868548d59..9ba3f205dc3 100644 --- a/src/Module.js +++ b/src/Module.js @@ -181,29 +181,20 @@ export default class Module { } // export { foo, bar, baz } - else { - if ( node.specifiers.length ) { - node.specifiers.forEach( specifier => { - const localName = specifier.local.name; - const exportedName = specifier.exported.name; - - if ( this.exports[ exportedName ] || this.reexports[ exportedName ] ) { - this.error({ - code: 'DUPLICATE_EXPORT', - message: `A module cannot have multiple exports with the same name ('${exportedName}')` - }, specifier.start ); - } - - this.exports[ exportedName ] = { localName }; - }); - } else { - // TODO is this really necessary? `export {}` is valid JS, and - // might be used as a hint that this is indeed a module - this.warn({ - code: 'EMPTY_EXPORT', - message: `Empty export declaration` - }, node.start ); - } + else if ( node.specifiers.length ) { + node.specifiers.forEach( specifier => { + const localName = specifier.local.name; + const exportedName = specifier.exported.name; + + if ( this.exports[ exportedName ] || this.reexports[ exportedName ] ) { + this.error({ + code: 'DUPLICATE_EXPORT', + message: `A module cannot have multiple exports with the same name ('${exportedName}')` + }, specifier.start ); + } + + this.exports[ exportedName ] = { localName }; + }); } } diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 5ab2954444d..23a007e986a 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -45,6 +45,9 @@ export default class MemberExpression extends Node { if ( !declaration ) { this.module.warn({ code: 'MISSING_EXPORT', + missing: part.name || part.value, + importer: relativeId( this.module.id ), + exporter: relativeId( exporterId ), message: `'${part.name || part.value}' is not exported by '${relativeId( exporterId )}'`, url: `https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module` }, part.start ); diff --git a/src/rollup.js b/src/rollup.js index 732cb5eab52..12eb5d44d6c 100644 --- a/src/rollup.js +++ b/src/rollup.js @@ -94,7 +94,7 @@ export function rollup ( options ) { function generate ( options = {} ) { if ( !options.format ) { - bundle.warn({ + bundle.warn({ // TODO make this an error code: 'MISSING_FORMAT', message: `No format option was supplied – defaulting to 'es'`, url: `https://github.com/rollup/rollup/wiki/JavaScript-API#format` From c378f20e08aaf87deda0506c1ac97826465b1266 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Jul 2017 14:52:38 -0400 Subject: [PATCH 3/8] various! --- bin/src/run/batchWarnings.js | 37 +++--- bin/src/run/build.js | 20 ++-- bin/src/run/createWatcher.js | 217 +++++++++++++++++++++++++++++++++++ bin/src/run/index.js | 111 ++++-------------- bin/src/run/mergeOptions.js | 84 ++++++++++++++ bin/src/run/watch.js | 41 +++---- bin/src/utils/sequence.js | 14 +++ package-lock.json | 100 +++++++++++++--- package.json | 6 +- rollup.config.cli.js | 3 +- 10 files changed, 473 insertions(+), 160 deletions(-) create mode 100644 bin/src/run/createWatcher.js create mode 100644 bin/src/run/mergeOptions.js create mode 100644 bin/src/utils/sequence.js diff --git a/bin/src/run/batchWarnings.js b/bin/src/run/batchWarnings.js index 86092cdab24..8ed81e5dada 100644 --- a/bin/src/run/batchWarnings.js +++ b/bin/src/run/batchWarnings.js @@ -1,9 +1,9 @@ import chalk from 'chalk'; -import { handleWarning } from '../logging.js'; +import { handleWarning, stderr } from '../logging.js'; import relativeId from '../../../src/utils/relativeId.js'; export default function batchWarnings () { - const allWarnings = new Map(); + let allWarnings = new Map(); let count = 0; return { @@ -44,6 +44,8 @@ export default function batchWarnings () { }); } }); + + allWarnings = new Map(); } }; } @@ -55,7 +57,7 @@ const handlers = { fn: warnings => { group( 'Unused external imports' ); warnings.forEach( warning => { - log( `${warning.message}` ); + stderr( `${warning.message}` ); }); } }, @@ -74,7 +76,7 @@ const handlers = { Array.from( dependencies.keys() ).forEach( dependency => { const importers = dependencies.get( dependency ); - log( `${chalk.bold( dependency )} (imported by ${importers.join( ', ' )})` ); + stderr( `${chalk.bold( dependency )} (imported by ${importers.join( ', ' )})` ); }); } }, @@ -86,9 +88,9 @@ const handlers = { info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module' ); warnings.forEach( warning => { - log( chalk.bold( warning.importer ) ); - log( `${warning.missing} is not exported by ${warning.exporter}` ); - log( chalk.grey( warning.frame ) ); + stderr( chalk.bold( warning.importer ) ); + stderr( `${warning.missing} is not exported by ${warning.exporter}` ); + stderr( chalk.grey( warning.frame ) ); }); } }, @@ -109,19 +111,18 @@ const handlers = { const ids = allIds.length > 5 ? allIds.slice( 0, 3 ) : allIds; ids.forEach( id => { - const allOccurrences = modules.get( id ); - const occurrences = allOccurrences.length > 5 ? allOccurrences.slice( 0, 3 ) : allOccurrences; + const occurrences = modules.get( id ); - log( chalk.bold( relativeId( id ) ) ); - log( chalk.grey( occurrences.map( warning => warning.frame ).join( '\n\n' ) ) ); + stderr( chalk.bold( relativeId( id ) ) ); + stderr( chalk.grey( occurrences[0].frame ) ); - if ( allOccurrences.length > occurrences.length ) { - log( `\n...and ${allOccurrences.length - occurrences.length} occurrences` ); + if ( occurrences.length > 1 ) { + stderr( `...and ${occurrences.length - 1} other ${occurrences.length > 2 ? 'occurrences' : 'occurrence'}` ); } }); if ( allIds.length > ids.length ) { - log( `\n...and ${allIds.length - ids.length} files` ); + stderr( `\n...and ${allIds.length - ids.length} other files` ); } } }, @@ -198,13 +199,9 @@ const handlers = { }; function group ( title ) { - log( `\n${chalk.bold.yellow('(!)')} ${chalk.bold.yellow( title )}` ); + stderr( `${chalk.bold.yellow('(!)')} ${chalk.bold.yellow( title )}` ); } function info ( url ) { - log( chalk.grey( url ) ); -} - -function log ( message ) { - console.warn( message ); // eslint-disable-line no-console + stderr( chalk.grey( url ) ); } \ No newline at end of file diff --git a/bin/src/run/build.js b/bin/src/run/build.js index 20ec941718d..055bdae386b 100644 --- a/bin/src/run/build.js +++ b/bin/src/run/build.js @@ -1,15 +1,11 @@ import * as rollup from 'rollup'; -import { handleError } from '../logging.js'; +import chalk from 'chalk'; +import { handleError, stderr } from '../logging.js'; import SOURCEMAPPING_URL from '../sourceMappingUrl.js'; -import batchWarnings from './batchWarnings.js'; -export default function build ( options ) { - let batch; - - if ( !options.onwarn ) { - batch = batchWarnings(); - options.onwarn = batch.add; - } +export default function build ( options, warnings ) { + const start = Date.now(); + stderr( chalk.green( `\n${chalk.bold( options.entry )} → ${chalk.bold( options.dest )}...` ) ); return rollup.rollup( options ) .then( bundle => { @@ -42,7 +38,11 @@ export default function build ( options ) { process.stdout.write( code ); }); }) - .then( batch.flush ) + .then( () => { + warnings.flush(); + stderr( chalk.green( `${chalk.bold( options.dest )} created in ${Date.now() - start}ms\n` ) ); + // stderr( `${chalk.blue( '----------' )}\n` ); + }) .catch( handleError ); } diff --git a/bin/src/run/createWatcher.js b/bin/src/run/createWatcher.js new file mode 100644 index 00000000000..4ec02fa5ff0 --- /dev/null +++ b/bin/src/run/createWatcher.js @@ -0,0 +1,217 @@ +import EventEmitter from 'events'; +import relative from 'require-relative'; +import path from 'path'; +import * as fs from 'fs'; +import createFilter from 'rollup-pluginutils/src/createFilter.js'; +import sequence from '../utils/sequence.js'; + +const opts = { encoding: 'utf-8', persistent: true }; + +let chokidar; + +try { + chokidar = relative( 'chokidar', process.cwd() ); +} catch (err) { + chokidar = null; +} + +class FileWatcher { + constructor ( file, data, callback, chokidarOptions, dispose ) { + const handleWatchEvent = (event) => { + if ( event === 'rename' || event === 'unlink' ) { + this.fsWatcher.close(); + dispose(); + callback(); + } else { + // this is necessary because we get duplicate events... + const contents = fs.readFileSync( file, 'utf-8' ); + if ( contents !== data ) { + data = contents; + callback(); + } + } + }; + + try { + if (chokidarOptions) { + this.fsWatcher = chokidar.watch(file, chokidarOptions).on('all', handleWatchEvent); + } else { + this.fsWatcher = fs.watch( file, opts, handleWatchEvent); + } + + this.fileExists = true; + } catch ( err ) { + if ( err.code === 'ENOENT' ) { + // can't watch files that don't exist (e.g. injected + // by plugins somehow) + this.fileExists = false; + } else { + throw err; + } + } + } + + close () { + this.fsWatcher.close(); + } +} + +export default function watch ( rollup, options ) { + const watchOptions = options.watch || {}; + + if ( 'useChokidar' in watchOptions ) watchOptions.chokidar = watchOptions.useChokidar; + let chokidarOptions = 'chokidar' in watchOptions ? watchOptions.chokidar : !!chokidar; + if ( chokidarOptions ) { + chokidarOptions = Object.assign( chokidarOptions === true ? {} : chokidarOptions, { + ignoreInitial: true + }); + } + + if ( chokidarOptions && !chokidar ) { + throw new Error( `options.watch.chokidar was provided, but chokidar could not be found. Have you installed it?` ); + } + + const watcher = new EventEmitter(); + + const filter = createFilter( watchOptions.include, watchOptions.exclude ); + const dests = options.dest ? [ path.resolve( options.dest ) ] : options.targets.map( target => path.resolve( target.dest ) ); + let filewatchers = new Map(); + + let rebuildScheduled = false; + let building = false; + let watching = false; + let closed = false; + + let timeout; + let cache; + + function triggerRebuild () { + clearTimeout( timeout ); + rebuildScheduled = true; + + timeout = setTimeout( () => { + if ( !building ) build(); + }, 50 ); + } + + function addFileWatchersForModules ( modules ) { + modules.forEach( module => { + let id = module.id; + + // skip plugin helper modules and unwatched files + if ( /\0/.test( id ) ) return; + if ( !filter( id ) ) return; + + try { + id = fs.realpathSync( id ); + } catch ( err ) { + return; + } + + if ( ~dests.indexOf( id ) ) { + throw new Error( 'Cannot import the generated bundle' ); + } + + if ( !filewatchers.has( id ) ) { + const watcher = new FileWatcher( id, module.originalCode, triggerRebuild, chokidarOptions, () => { + filewatchers.delete( id ); + }); + + if ( watcher.fileExists ) filewatchers.set( id, watcher ); + } + }); + } + + function build () { + if ( building || closed ) return; + + rebuildScheduled = false; + + let start = Date.now(); + let initial = !watching; + if ( cache ) options.cache = cache; + + watcher.emit( 'event', { code: 'BUILD_START' }); + + building = true; + + return rollup.rollup( options ) + .then( bundle => { + // Save off bundle for re-use later + cache = bundle; + + if ( !closed ) { + addFileWatchersForModules(bundle.modules); + } + + // Now we're watching + watching = true; + + if ( options.targets ) { + return sequence( options.targets, target => { + const mergedOptions = Object.assign( {}, options, target ); + return bundle.write( mergedOptions ); + }); + } + + return bundle.write( options ); + }) + .then( () => { + watcher.emit( 'event', { + code: 'BUILD_END', + duration: Date.now() - start, + initial + }); + }, error => { + try { + //If build failed, make sure we are still watching those files from the most recent successful build. + addFileWatchersForModules( cache.modules ); + } + catch (e) { + //Ignore if they tried to import the output. We are already inside of a catch (probably caused by that). + } + watcher.emit( 'event', { + code: 'ERROR', + error + }); + }) + .then( () => { + building = false; + if ( rebuildScheduled && !closed ) build(); + }); + } + + // build on next tick, so consumers can listen for BUILD_START + process.nextTick( build ); + + function close () { + if ( closed ) return; + for ( const fw of filewatchers.values() ) { + fw.close(); + } + + process.removeListener('SIGINT', close); + process.removeListener('SIGTERM', close); + process.removeListener('uncaughtException', close); + process.stdin.removeListener('end', close); + + watcher.removeAllListeners(); + closed = true; + } + + watcher.close = close; + + // ctrl-c + process.on('SIGINT', close); + + // killall node + process.on('SIGTERM', close); + + // on error + process.on('uncaughtException', close); + + // in case we ever support stdin! + process.stdin.on('end', close); + + return watcher; +} diff --git a/bin/src/run/index.js b/bin/src/run/index.js index 54854fca739..06ac892c806 100644 --- a/bin/src/run/index.js +++ b/bin/src/run/index.js @@ -3,7 +3,9 @@ import { realpathSync } from 'fs'; import * as rollup from 'rollup'; import relative from 'require-relative'; import { handleWarning, handleError } from '../logging.js'; +import mergeOptions from './mergeOptions.js'; import batchWarnings from './batchWarnings.js'; +import sequence from '../utils/sequence.js'; import build from './build.js'; import watch from './watch.js'; @@ -66,25 +68,23 @@ export default function runRollup ( command ) { config = realpathSync( config ); } - const batch = batchWarnings(); + const warnings = batchWarnings(); rollup.rollup({ entry: config, external: id => { return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5,id.length) === '.json'; }, - onwarn: batch.add + onwarn: warnings.add }) .then( bundle => { - batch.flush(); + warnings.flush(); return bundle.generate({ format: 'cjs' }); }) .then( ({ code }) => { - if ( command.watch ) process.env.ROLLUP_WATCH = 'true'; - // temporarily override require const defaultLoader = require.extensions[ '.js' ]; require.extensions[ '.js' ] = ( m, filename ) => { @@ -96,96 +96,33 @@ export default function runRollup ( command ) { }; const configs = require( config ); - const normalized = Array.isArray( configs ) ? configs : [configs]; - - normalized.forEach(options => { - if ( Object.keys( options ).length === 0 ) { - handleError({ - code: 'MISSING_CONFIG', - message: 'Config file must export an options object', - url: 'https://github.com/rollup/rollup/wiki/Command-Line-Interface#using-a-config-file' - }); - } - - execute( options, command ); - }); + if ( Object.keys( configs ).length === 0 ) { + handleError({ + code: 'MISSING_CONFIG', + message: 'Config file must export an options object, or an array of options objects', + url: 'https://github.com/rollup/rollup/wiki/Command-Line-Interface#using-a-config-file' + }); + } require.extensions[ '.js' ] = defaultLoader; + + const normalized = Array.isArray( configs ) ? configs : [configs]; + return execute( normalized, command ); }) .catch( handleError ); } else { - execute( {}, command ); + return execute( [{}], command ); } } -const equivalents = { - useStrict: 'useStrict', - banner: 'banner', - footer: 'footer', - format: 'format', - globals: 'globals', - id: 'moduleId', - indent: 'indent', - input: 'entry', - intro: 'intro', - legacy: 'legacy', - name: 'moduleName', - output: 'dest', - outro: 'outro', - sourcemap: 'sourceMap', - treeshake: 'treeshake' -}; - -function execute ( options, command ) { - let external; - - const commandExternal = ( command.external || '' ).split( ',' ); - const optionsExternal = options.external; - - if ( command.globals ) { - const globals = Object.create( null ); - - command.globals.split( ',' ).forEach( str => { - const names = str.split( ':' ); - globals[ names[0] ] = names[1]; - - // Add missing Module IDs to external. - if ( commandExternal.indexOf( names[0] ) === -1 ) { - commandExternal.push( names[0] ); - } - }); - - command.globals = globals; - } - - if ( typeof optionsExternal === 'function' ) { - external = id => { - return optionsExternal( id ) || ~commandExternal.indexOf( id ); - }; - } else { - external = ( optionsExternal || [] ).concat( commandExternal ); - } - - if (typeof command.extend !== 'undefined') { - options.extend = command.extend; - } - - if ( command.silent ) { - options.onwarn = () => {}; - } - - options.external = external; - - // Use any options passed through the CLI as overrides. - Object.keys( equivalents ).forEach( cliOption => { - if ( command.hasOwnProperty( cliOption ) ) { - options[ equivalents[ cliOption ] ] = command[ cliOption ]; - } - }); - +function execute ( configs, command ) { if ( command.watch ) { - watch( options ); + process.env.ROLLUP_WATCH = 'true'; + watch( configs, command ); } else { - build( options ).catch( handleError ); + return sequence( config => { + const { options, warnings } = mergeOptions( config, command ); + return build( options, warnings ); + }); } -} +} \ No newline at end of file diff --git a/bin/src/run/mergeOptions.js b/bin/src/run/mergeOptions.js new file mode 100644 index 00000000000..c56e08e98f8 --- /dev/null +++ b/bin/src/run/mergeOptions.js @@ -0,0 +1,84 @@ +import batchWarnings from './batchWarnings.js'; + +const equivalents = { + useStrict: 'useStrict', + banner: 'banner', + footer: 'footer', + format: 'format', + globals: 'globals', + id: 'moduleId', + indent: 'indent', + input: 'entry', + intro: 'intro', + legacy: 'legacy', + name: 'moduleName', + output: 'dest', + outro: 'outro', + sourcemap: 'sourceMap', + treeshake: 'treeshake' +}; + +export default function mergeOptions ( config, command ) { + const options = Object.assign( {}, config ); + + let external; + + const commandExternal = ( command.external || '' ).split( ',' ); + const optionsExternal = options.external; + + if ( command.globals ) { + const globals = Object.create( null ); + + command.globals.split( ',' ).forEach( str => { + const names = str.split( ':' ); + globals[ names[0] ] = names[1]; + + // Add missing Module IDs to external. + if ( commandExternal.indexOf( names[0] ) === -1 ) { + commandExternal.push( names[0] ); + } + }); + + command.globals = globals; + } + + if ( typeof optionsExternal === 'function' ) { + external = id => { + return optionsExternal( id ) || ~commandExternal.indexOf( id ); + }; + } else { + external = ( optionsExternal || [] ).concat( commandExternal ); + } + + if (typeof command.extend !== 'undefined') { + options.extend = command.extend; + } + + const warnings = batchWarnings(); + + if ( command.silent ) { + options.onwarn = () => {}; + } else { + options.onwarn = warnings.add; + + const onwarn = options.onwarn; + if ( onwarn ) { + options.onwarn = warning => { + onwarn( warning, warnings.add ); + }; + } else { + options.onwarn = warnings.add; + } + } + + options.external = external; + + // Use any options passed through the CLI as overrides. + Object.keys( equivalents ).forEach( cliOption => { + if ( command.hasOwnProperty( cliOption ) ) { + options[ equivalents[ cliOption ] ] = command[ cliOption ]; + } + }); + + return { options, warnings }; +} \ No newline at end of file diff --git a/bin/src/run/watch.js b/bin/src/run/watch.js index 783c8aba168..9bd5ff90f33 100644 --- a/bin/src/run/watch.js +++ b/bin/src/run/watch.js @@ -1,18 +1,21 @@ import * as rollup from 'rollup'; -import relative from 'require-relative'; +import chalk from 'chalk'; +import createWatcher from './createWatcher.js'; +import mergeOptions from './mergeOptions.js'; import { handleError, stderr } from '../logging.js'; -export default function watch ( options ) { - if ( !options.entry || ( !options.dest && !options.targets ) ) { - handleError({ - code: 'WATCHER_MISSING_INPUT_OR_OUTPUT', - message: 'must specify --input and --output when using rollup --watch' - }); - } +export default function watch ( configs, command ) { + configs.forEach( config => { + const { options, warnings } = mergeOptions( config, command ); + + if ( !options.entry || ( !options.dest && !options.targets ) ) { + handleError({ + code: 'WATCHER_MISSING_INPUT_OR_OUTPUT', + message: 'must specify --input and --output when using rollup --watch' + }); + } - try { - const watch = relative( 'rollup-watch', process.cwd() ); - const watcher = watch( rollup, options ); + const watcher = createWatcher( rollup, options ); watcher.on( 'event', event => { switch ( event.code ) { @@ -21,11 +24,12 @@ export default function watch ( options ) { break; case 'BUILD_START': - stderr( 'bundling...' ); + stderr( `${chalk.blue.bold( options.entry )} -> ${chalk.blue.bold( options.dest )}...` ); break; case 'BUILD_END': - stderr( 'bundled in ' + event.duration + 'ms. Watching for changes...' ); + warnings.flush(); + stderr( `created ${chalk.blue.bold( options.dest )} in ${event.duration}ms. Watching for changes...` ); break; case 'ERROR': @@ -36,14 +40,5 @@ export default function watch ( options ) { stderr( 'unknown event', event ); } }); - } catch ( err ) { - if ( err.code === 'MODULE_NOT_FOUND' ) { - handleError({ - code: 'ROLLUP_WATCH_NOT_INSTALLED', - message: 'rollup --watch depends on the rollup-watch package, which could not be found. Install it with npm install -D rollup-watch' - }); - } - - handleError( err ); - } + }); } \ No newline at end of file diff --git a/bin/src/utils/sequence.js b/bin/src/utils/sequence.js new file mode 100644 index 00000000000..6f0db0c3ed3 --- /dev/null +++ b/bin/src/utils/sequence.js @@ -0,0 +1,14 @@ +export default function sequence ( array, fn ) { + const results = []; + let promise = Promise.resolve(); + + function next ( member, i ) { + return fn( member ).then( value => results[i] = value ); + } + + for ( let i = 0; i < array.length; i += 1 ) { + promise = promise.then( () => next( array[i], i ) ); + } + + return promise.then( () => results ); +} diff --git a/package-lock.json b/package-lock.json index 089b8bde5ef..500e3f2c595 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "0.45.1", + "version": "0.45.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -405,6 +405,17 @@ "has-ansi": "2.0.0", "strip-ansi": "3.0.1", "supports-color": "2.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + } } }, "chokidar": { @@ -2409,6 +2420,17 @@ "string-width": "1.0.2", "strip-ansi": "3.0.1", "through": "2.3.8" + }, + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + } } }, "interpret": { @@ -3811,6 +3833,18 @@ "requires": { "buble": "0.15.2", "rollup-pluginutils": "1.5.2" + }, + "dependencies": { + "rollup-pluginutils": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz", + "integrity": "sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=", + "dev": true, + "requires": { + "estree-walker": "0.2.1", + "minimatch": "3.0.4" + } + } } }, "rollup-plugin-commonjs": { @@ -3917,6 +3951,16 @@ "requires": { "vlq": "0.2.2" } + }, + "rollup-pluginutils": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz", + "integrity": "sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=", + "dev": true, + "requires": { + "estree-walker": "0.2.1", + "minimatch": "3.0.4" + } } } }, @@ -3927,16 +3971,36 @@ "dev": true, "requires": { "rollup-pluginutils": "1.5.2" + }, + "dependencies": { + "rollup-pluginutils": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz", + "integrity": "sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=", + "dev": true, + "requires": { + "estree-walker": "0.2.1", + "minimatch": "3.0.4" + } + } } }, "rollup-pluginutils": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz", - "integrity": "sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.0.1.tgz", + "integrity": "sha1-fslbNXP2VDpGpkYb2afFRFJdD8A=", "dev": true, "requires": { - "estree-walker": "0.2.1", - "minimatch": "3.0.4" + "estree-walker": "0.3.1", + "micromatch": "2.3.11" + }, + "dependencies": { + "estree-walker": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.3.1.tgz", + "integrity": "sha1-5rGlHPcpJSTnI3wxLl/mZgwc4ao=", + "dev": true + } } }, "rollup-watch": { @@ -4048,12 +4112,14 @@ "source-map": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "dev": true }, "source-map-support": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=", + "dev": true, "requires": { "source-map": "0.5.6" } @@ -4133,6 +4199,17 @@ "code-point-at": "1.1.0", "is-fullwidth-code-point": "1.0.0", "strip-ansi": "3.0.1" + }, + "dependencies": { + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + } } }, "stringstream": { @@ -4142,15 +4219,6 @@ "dev": true, "optional": true }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", diff --git a/package.json b/package.json index f2a44f3d1ba..c797aa6346c 100644 --- a/package.json +++ b/package.json @@ -67,15 +67,15 @@ "rollup-plugin-node-resolve": "^3.0.0", "rollup-plugin-replace": "^1.1.0", "rollup-plugin-string": "^2.0.0", + "rollup-pluginutils": "^2.0.1", "rollup-watch": "^4.3.1", "sander": "^0.6.0", "source-map": "^0.5.6", + "source-map-support": "^0.4.15", "sourcemap-codec": "^1.3.0", "uglify-js": "^3.0.19" }, - "dependencies": { - "source-map-support": "^0.4.0" - }, + "dependencies": {}, "files": [ "dist", "bin/rollup", diff --git a/rollup.config.cli.js b/rollup.config.cli.js index d8dae2a69eb..a6902314a59 100644 --- a/rollup.config.cli.js +++ b/rollup.config.cli.js @@ -12,7 +12,7 @@ export default { plugins: [ string({ include: '**/*.md' }), json(), - buble(), + buble({ target: { node: 4 } }), commonjs({ include: 'node_modules/**' }), @@ -24,6 +24,7 @@ export default { 'fs', 'path', 'module', + 'events', 'source-map-support', 'rollup' ], From c35648fab5287755741148a48cb00c6c8dba3cad Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 8 Aug 2017 22:19:14 -0400 Subject: [PATCH 4/8] update some tests --- .../custom-path-resolver-async/_config.js | 2 ++ .../custom-path-resolver-sync/_config.js | 2 ++ .../_config.js | 2 ++ test/function/empty-exports/_config.js | 23 ------------------- test/function/empty-exports/main.js | 1 - .../namespace-missing-export/_config.js | 3 +++ test/function/unused-import/_config.js | 2 ++ test/mocha.opts | 1 - 8 files changed, 11 insertions(+), 25 deletions(-) delete mode 100644 test/function/empty-exports/_config.js delete mode 100644 test/function/empty-exports/main.js diff --git a/test/function/custom-path-resolver-async/_config.js b/test/function/custom-path-resolver-async/_config.js index c471e027b8f..27b46dc9f9d 100644 --- a/test/function/custom-path-resolver-async/_config.js +++ b/test/function/custom-path-resolver-async/_config.js @@ -23,6 +23,8 @@ module.exports = { warnings: [ { code: 'UNRESOLVED_IMPORT', + importer: 'main.js', + source: 'path', message: `'path' is imported by main.js, but could not be resolved – treating it as an external dependency`, url: `https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency` } diff --git a/test/function/custom-path-resolver-sync/_config.js b/test/function/custom-path-resolver-sync/_config.js index f32cb9ab7b3..49c926ef1fe 100644 --- a/test/function/custom-path-resolver-sync/_config.js +++ b/test/function/custom-path-resolver-sync/_config.js @@ -16,6 +16,8 @@ module.exports = { warnings: [ { code: 'UNRESOLVED_IMPORT', + importer: 'main.js', + source: 'path', message: `'path' is imported by main.js, but could not be resolved – treating it as an external dependency`, url: `https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency` } diff --git a/test/function/does-not-hang-on-missing-module/_config.js b/test/function/does-not-hang-on-missing-module/_config.js index aeb5386224e..6540af6f5f8 100644 --- a/test/function/does-not-hang-on-missing-module/_config.js +++ b/test/function/does-not-hang-on-missing-module/_config.js @@ -5,6 +5,8 @@ module.exports = { warnings: [ { code: 'UNRESOLVED_IMPORT', + importer: 'main.js', + source: 'unlessYouCreatedThisFileForSomeReason', message: `'unlessYouCreatedThisFileForSomeReason' is imported by main.js, but could not be resolved – treating it as an external dependency`, url: `https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency` } diff --git a/test/function/empty-exports/_config.js b/test/function/empty-exports/_config.js deleted file mode 100644 index e928a59b106..00000000000 --- a/test/function/empty-exports/_config.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - description: 'warns on export {}, but does not fail', - warnings: [ - { - code: 'EMPTY_EXPORT', - message: 'Empty export declaration', - pos: 0, - loc: { - file: require( 'path' ).resolve( __dirname, 'main.js' ), - line: 1, - column: 0 - }, - frame: ` - 1: export {}; - ^ - ` - }, - { - code: 'EMPTY_BUNDLE', - message: 'Generated an empty bundle' - } - ] -}; diff --git a/test/function/empty-exports/main.js b/test/function/empty-exports/main.js deleted file mode 100644 index cb0ff5c3b54..00000000000 --- a/test/function/empty-exports/main.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/test/function/namespace-missing-export/_config.js b/test/function/namespace-missing-export/_config.js index 485a1ba17bd..6848cc28686 100644 --- a/test/function/namespace-missing-export/_config.js +++ b/test/function/namespace-missing-export/_config.js @@ -2,6 +2,9 @@ module.exports = { warnings: [ { code: 'MISSING_EXPORT', + exporter: 'empty.js', + importer: 'main.js', + missing: 'foo', message: `'foo' is not exported by 'empty.js'`, pos: 61, loc: { diff --git a/test/function/unused-import/_config.js b/test/function/unused-import/_config.js index 0f99f594b94..a60eaf85acf 100644 --- a/test/function/unused-import/_config.js +++ b/test/function/unused-import/_config.js @@ -5,6 +5,8 @@ module.exports = { warnings: [ { code: 'UNRESOLVED_IMPORT', + importer: 'main.js', + source: 'external', message: `'external' is imported by main.js, but could not be resolved – treating it as an external dependency`, url: `https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency` }, diff --git a/test/mocha.opts b/test/mocha.opts index fcb33f02a08..0339aeebdc1 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,2 @@ ---bail --compilers js:buble/register test/test.js \ No newline at end of file From 1f31de4a60eba8afd679f055d6835caca2fb13fa Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 9 Aug 2017 15:42:51 -0400 Subject: [PATCH 5/8] implement rollup.watch --- .gitignore | 1 + src/rollup.js | 186 +-------------------- src/rollup/index.js | 184 +++++++++++++++++++++ src/watch/chokidar.js | 11 ++ src/watch/fileWatchers.js | 88 ++++++++++ src/watch/index.js | 153 +++++++++++++++++ test/test.js | 254 +++++++++++++++++++++++++++++ test/watch/samples/basic/main.js | 1 + test/watch/samples/ignored/bar.js | 1 + test/watch/samples/ignored/foo.js | 1 + test/watch/samples/ignored/main.js | 4 + 11 files changed, 700 insertions(+), 184 deletions(-) create mode 100644 src/rollup/index.js create mode 100644 src/watch/chokidar.js create mode 100644 src/watch/fileWatchers.js create mode 100644 src/watch/index.js create mode 100644 test/watch/samples/basic/main.js create mode 100644 test/watch/samples/ignored/bar.js create mode 100644 test/watch/samples/ignored/foo.js create mode 100644 test/watch/samples/ignored/main.js diff --git a/.gitignore b/.gitignore index f5abf8d2c85..e19d49a9cf5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ coverage .commithash .idea bin/rollup +test/_tmp \ No newline at end of file diff --git a/src/rollup.js b/src/rollup.js index 12eb5d44d6c..7845ad0145c 100644 --- a/src/rollup.js +++ b/src/rollup.js @@ -1,184 +1,2 @@ -import { timeStart, timeEnd, flushTime } from './utils/flushTime.js'; -import { basename } from './utils/path.js'; -import { writeFile } from './utils/fs.js'; -import { assign, keys } from './utils/object.js'; -import { mapSequence } from './utils/promise.js'; -import validateKeys from './utils/validateKeys.js'; -import error from './utils/error.js'; -import { SOURCEMAPPING_URL } from './utils/sourceMappingURL.js'; -import Bundle from './Bundle.js'; - -export const VERSION = '<@VERSION@>'; - -const ALLOWED_KEYS = [ - 'acorn', - 'amd', - 'banner', - 'cache', - 'context', - 'dest', - 'entry', - 'exports', - 'extend', - 'external', - 'footer', - 'format', - 'globals', - 'indent', - 'interop', - 'intro', - 'legacy', - 'moduleContext', - 'moduleName', - 'noConflict', - 'onwarn', - 'outro', - 'paths', - 'plugins', - 'preferConst', - 'pureExternalModules', - 'sourceMap', - 'sourceMapFile', - 'targets', - 'treeshake', - 'useStrict', - 'watch' -]; - -function checkAmd ( options ) { - if ( options.moduleId ) { - if ( options.amd ) throw new Error( 'Cannot have both options.amd and options.moduleId' ); - - options.amd = { id: options.moduleId }; - delete options.moduleId; - - const msg = `options.moduleId is deprecated in favour of options.amd = { id: moduleId }`; - if ( options.onwarn ) { - options.onwarn( msg ); - } else { - console.warn( msg ); // eslint-disable-line no-console - } - } -} - -function checkOptions ( options ) { - if ( !options ) { - throw new Error( 'You must supply an options object to rollup' ); - } - - if ( options.transform || options.load || options.resolveId || options.resolveExternal ) { - throw new Error( 'The `transform`, `load`, `resolveId` and `resolveExternal` options are deprecated in favour of a unified plugin API. See https://github.com/rollup/rollup/wiki/Plugins for details' ); - } - - checkAmd (options); - - const err = validateKeys( keys(options), ALLOWED_KEYS ); - if ( err ) throw err; -} - -const throwAsyncGenerateError = { - get () { - throw new Error( `bundle.generate(...) now returns a Promise instead of a { code, map } object` ); - } -}; - -export function rollup ( options ) { - try { - checkOptions( options ); - const bundle = new Bundle( options ); - - timeStart( '--BUILD--' ); - - return bundle.build().then( () => { - timeEnd( '--BUILD--' ); - - function generate ( options = {} ) { - if ( !options.format ) { - bundle.warn({ // TODO make this an error - code: 'MISSING_FORMAT', - message: `No format option was supplied – defaulting to 'es'`, - url: `https://github.com/rollup/rollup/wiki/JavaScript-API#format` - }); - - options.format = 'es'; - } - - checkAmd( options ); - - timeStart( '--GENERATE--' ); - - const promise = Promise.resolve() - .then( () => bundle.render( options ) ) - .then( rendered => { - timeEnd( '--GENERATE--' ); - - bundle.plugins.forEach( plugin => { - if ( plugin.ongenerate ) { - plugin.ongenerate( assign({ - bundle: result - }, options ), rendered); - } - }); - - flushTime(); - - return rendered; - }); - - Object.defineProperty( promise, 'code', throwAsyncGenerateError ); - Object.defineProperty( promise, 'map', throwAsyncGenerateError ); - - return promise; - } - - const result = { - imports: bundle.externalModules.map( module => module.id ), - exports: keys( bundle.entryModule.exports ), - modules: bundle.orderedModules.map( module => module.toJSON() ), - - generate, - write: options => { - if ( !options || !options.dest ) { - error({ - code: 'MISSING_OPTION', - message: 'You must supply options.dest to bundle.write' - }); - } - - const dest = options.dest; - return generate( options ).then( output => { - let { code, map } = output; - - const promises = []; - - if ( options.sourceMap ) { - let url; - - if ( options.sourceMap === 'inline' ) { - url = map.toUrl(); - } else { - url = `${basename( dest )}.map`; - promises.push( writeFile( dest + '.map', map.toString() ) ); - } - - code += `//# ${SOURCEMAPPING_URL}=${url}\n`; - } - - promises.push( writeFile( dest, code ) ); - return Promise.all( promises ).then( () => { - return mapSequence( bundle.plugins.filter( plugin => plugin.onwrite ), plugin => { - return Promise.resolve( plugin.onwrite( assign({ - bundle: result - }, options ), output)); - }); - }); - }); - } - }; - - return result; - }); - } catch ( err ) { - return Promise.reject( err ); - } -} +export { default as rollup } from './rollup/index.js'; +export { default as watch } from './watch/index.js'; \ No newline at end of file diff --git a/src/rollup/index.js b/src/rollup/index.js new file mode 100644 index 00000000000..7213c1ea1f8 --- /dev/null +++ b/src/rollup/index.js @@ -0,0 +1,184 @@ +import { timeStart, timeEnd, flushTime } from '../utils/flushTime.js'; +import { basename } from '../utils/path.js'; +import { writeFile } from '../utils/fs.js'; +import { assign, keys } from '../utils/object.js'; +import { mapSequence } from '../utils/promise.js'; +import validateKeys from '../utils/validateKeys.js'; +import error from '../utils/error.js'; +import { SOURCEMAPPING_URL } from '../utils/sourceMappingURL.js'; +import Bundle from '../Bundle.js'; + +export const VERSION = '<@VERSION@>'; + +const ALLOWED_KEYS = [ + 'acorn', + 'amd', + 'banner', + 'cache', + 'context', + 'dest', + 'entry', + 'exports', + 'extend', + 'external', + 'footer', + 'format', + 'globals', + 'indent', + 'interop', + 'intro', + 'legacy', + 'moduleContext', + 'moduleName', + 'noConflict', + 'onwarn', + 'outro', + 'paths', + 'plugins', + 'preferConst', + 'pureExternalModules', + 'sourceMap', + 'sourceMapFile', + 'targets', + 'treeshake', + 'useStrict', + 'watch' +]; + +function checkAmd ( options ) { + if ( options.moduleId ) { + if ( options.amd ) throw new Error( 'Cannot have both options.amd and options.moduleId' ); + + options.amd = { id: options.moduleId }; + delete options.moduleId; + + const msg = `options.moduleId is deprecated in favour of options.amd = { id: moduleId }`; + if ( options.onwarn ) { + options.onwarn( msg ); + } else { + console.warn( msg ); // eslint-disable-line no-console + } + } +} + +function checkOptions ( options ) { + if ( !options ) { + throw new Error( 'You must supply an options object to rollup' ); + } + + if ( options.transform || options.load || options.resolveId || options.resolveExternal ) { + throw new Error( 'The `transform`, `load`, `resolveId` and `resolveExternal` options are deprecated in favour of a unified plugin API. See https://github.com/rollup/rollup/wiki/Plugins for details' ); + } + + checkAmd (options); + + const err = validateKeys( keys(options), ALLOWED_KEYS ); + if ( err ) throw err; +} + +const throwAsyncGenerateError = { + get () { + throw new Error( `bundle.generate(...) now returns a Promise instead of a { code, map } object` ); + } +}; + +export default function rollup ( options ) { + try { + checkOptions( options ); + const bundle = new Bundle( options ); + + timeStart( '--BUILD--' ); + + return bundle.build().then( () => { + timeEnd( '--BUILD--' ); + + function generate ( options = {} ) { + if ( !options.format ) { + bundle.warn({ // TODO make this an error + code: 'MISSING_FORMAT', + message: `No format option was supplied – defaulting to 'es'`, + url: `https://github.com/rollup/rollup/wiki/JavaScript-API#format` + }); + + options.format = 'es'; + } + + checkAmd( options ); + + timeStart( '--GENERATE--' ); + + const promise = Promise.resolve() + .then( () => bundle.render( options ) ) + .then( rendered => { + timeEnd( '--GENERATE--' ); + + bundle.plugins.forEach( plugin => { + if ( plugin.ongenerate ) { + plugin.ongenerate( assign({ + bundle: result + }, options ), rendered); + } + }); + + flushTime(); + + return rendered; + }); + + Object.defineProperty( promise, 'code', throwAsyncGenerateError ); + Object.defineProperty( promise, 'map', throwAsyncGenerateError ); + + return promise; + } + + const result = { + imports: bundle.externalModules.map( module => module.id ), + exports: keys( bundle.entryModule.exports ), + modules: bundle.orderedModules.map( module => module.toJSON() ), + + generate, + write: options => { + if ( !options || !options.dest ) { + error({ + code: 'MISSING_OPTION', + message: 'You must supply options.dest to bundle.write' + }); + } + + const dest = options.dest; + return generate( options ).then( output => { + let { code, map } = output; + + const promises = []; + + if ( options.sourceMap ) { + let url; + + if ( options.sourceMap === 'inline' ) { + url = map.toUrl(); + } else { + url = `${basename( dest )}.map`; + promises.push( writeFile( dest + '.map', map.toString() ) ); + } + + code += `//# ${SOURCEMAPPING_URL}=${url}\n`; + } + + promises.push( writeFile( dest, code ) ); + return Promise.all( promises ).then( () => { + return mapSequence( bundle.plugins.filter( plugin => plugin.onwrite ), plugin => { + return Promise.resolve( plugin.onwrite( assign({ + bundle: result + }, options ), output)); + }); + }); + }); + } + }; + + return result; + }); + } catch ( err ) { + return Promise.reject( err ); + } +} diff --git a/src/watch/chokidar.js b/src/watch/chokidar.js new file mode 100644 index 00000000000..18ad0a5540f --- /dev/null +++ b/src/watch/chokidar.js @@ -0,0 +1,11 @@ +import relative from 'require-relative'; + +let chokidar; + +try { + chokidar = relative( 'chokidar', process.cwd() ); +} catch (err) { + chokidar = null; +} + +export default chokidar; \ No newline at end of file diff --git a/src/watch/fileWatchers.js b/src/watch/fileWatchers.js new file mode 100644 index 00000000000..0070f6572af --- /dev/null +++ b/src/watch/fileWatchers.js @@ -0,0 +1,88 @@ + +import * as fs from 'fs'; +import chokidar from './chokidar.js'; + +const opts = { encoding: 'utf-8', persistent: true }; + +const watchers = new Map(); + +export function addTask(id, task) { + if (!watchers.has(id)) { + const watcher = new FileWatcher(id, () => { + watchers.delete(id); + }); + + if (watcher.fileExists) { + watchers.set(id, watcher); + } else { + return; + } + } + + watchers.get(id).tasks.add(task); +} + +export function deleteTask(id, target) { + const watcher = watchers.get(id); + watcher.tasks.delete(target); + + if (watcher.tasks.size === 0) { + watcher.close(); + watchers.delete(id); + } +} + +export default class FileWatcher { + constructor(id, chokidarOptions, dispose) { + this.tasks = new Set(); + + let data; + + try { + fs.statSync(id); + this.fileExists = true; + } catch (err) { + if (err.code === 'ENOENT') { + // can't watch files that don't exist (e.g. injected + // by plugins somehow) + this.fileExists = false; + return; + } else { + throw err; + } + } + + const handleWatchEvent = event => { + if (event === 'rename' || event === 'unlink') { + this.fsWatcher.close(); + dispose(); + this.trigger(); + } else { + // this is necessary because we get duplicate events... + const contents = fs.readFileSync(id, 'utf-8'); + if (contents !== data) { + data = contents; + this.trigger(); + } + } + }; + + if (chokidarOptions) { + this.fsWatcher = chokidar + .watch(id, chokidarOptions) + .on('all', handleWatchEvent); + } else { + this.fsWatcher = fs.watch(id, opts, handleWatchEvent); + } + } + + close() { + this.fsWatcher.close(); + } + + trigger() { + this.tasks.forEach(task => { + task.makeDirty(); + }); + } +} diff --git a/src/watch/index.js b/src/watch/index.js new file mode 100644 index 00000000000..52cb823e61a --- /dev/null +++ b/src/watch/index.js @@ -0,0 +1,153 @@ +import path from 'path'; +import EventEmitter from 'events'; +import rollup from '../rollup/index.js'; +import ensureArray from '../utils/ensureArray.js'; +import { mapSequence } from '../utils/promise.js'; +import { addTask, deleteTask } from './fileWatchers.js'; + +const DELAY = 100; + +class Watcher extends EventEmitter { + constructor(configs) { + super(); + + this.dirty = true; + this.running = false; + this.tasks = ensureArray(configs).map(config => new Task(this, config)); + + process.nextTick(() => { + this.run(); + }); + } + + close() { + this.tasks.forEach(task => { + task.close(); + }); + } + + error(error) { + this.emit('event', { + code: 'ERROR', + error + }); + } + + makeDirty() { + if (this.dirty) return; + this.dirty = true; + + if (!this.running) { + setTimeout(() => { + this.run(); + }, DELAY); + } + } + + run() { + this.running = true; + this.dirty = false; + + // TODO + this.emit('event', { + code: 'BUILD_START' + }); + + mapSequence(this.tasks, task => { + return task.run().catch(error => { + this.emit('event', { + code: 'ERROR', + error + }); + }); + }).then(() => { + this.running = false; + + this.emit('event', { + code: 'BUILD_END' + }); + + if (this.dirty) this.run(); + }); + } +} + +class Task { + constructor(watcher, config) { + this.cache = null; + this.watcher = watcher; + this.config = config; + + this.closed = false; + this.watched = new Set(); + + this.dests = new Set( + (config.dest ? [config.dest] : config.targets.map(t => t.dest)).map(dest => path.resolve(dest)) + ); + } + + close() { + this.closed = true; + this.watched.forEach(id => { + deleteTask(id, this); + }); + } + + makeDirty() { + if (!this.dirty) { + this.dirty = true; + this.watcher.makeDirty(); + } + } + + run() { + this.dirty = false; + + const config = Object.assign(this.config, { + cache: this.cache + }); + + return rollup(config).then(bundle => { + if (this.closed) return; + + this.cache = bundle; + + const watched = new Set(); + + bundle.modules.forEach(module => { + if (this.dests.has(module.id)) { + throw new Error('Cannot import the generated bundle'); + } + + watched.add(module.id); + addTask(module.id, this); + }); + + this.watched.forEach(id => { + if (!watched.has(id)) deleteTask(id, this); + }); + + this.watched = watched; + + if (this.config.dest) { + return bundle.write({ + format: this.config.format, + dest: this.config.dest + }); + } + + return Promise.all( + this.config.targets.map(target => { + return bundle.write({ + format: target.format, + dest: target.dest + }); + }) + ); + }); + } +} + +export default function watch(configs) { + return new Watcher(configs); +} \ No newline at end of file diff --git a/test/test.js b/test/test.js index 1afe24fc399..a1a071caece 100644 --- a/test/test.js +++ b/test/test.js @@ -14,6 +14,8 @@ const FORM = path.resolve( __dirname, 'form' ); const SOURCEMAPS = path.resolve( __dirname, 'sourcemaps' ); const CLI = path.resolve( __dirname, 'cli' ); +const cwd = process.cwd(); + const PROFILES = [ { format: 'amd' }, { format: 'cjs' }, @@ -100,6 +102,12 @@ function compareError ( actual, expected ) { assert.deepEqual( actual, expected ); } +function wait ( ms ) { + return new Promise( fulfil => { + setTimeout( fulfil, ms ); + }); +} + describe( 'rollup', function () { this.timeout( 10000 ); @@ -951,4 +959,250 @@ describe( 'rollup', function () { }); }); }); + + describe.only( 'rollup.watch', () => { + beforeEach( () => { + process.chdir(cwd); + return sander.rimraf( 'test/_tmp' ); + }); + + function run ( file ) { + const resolved = require.resolve( file ); + delete require.cache[ resolved ]; + return require( resolved ); + } + + function sequence ( watcher, events ) { + return new Promise( ( fulfil, reject ) => { + function go ( event ) { + const next = events.shift(); + + if ( !next ) { + fulfil(); + } + + else if ( typeof next === 'string' ) { + watcher.once( 'event', event => { + if ( event.code !== next ) { + reject( new Error( `Expected ${next} error, got ${event.code}` ) ); + } else { + go( event ); + } + }); + } + + else { + Promise.resolve() + .then( () => wait( 100 ) ) // gah, this appears to be necessary to fix random errors + .then( () => next( event ) ) + .then( go ) + .catch( reject ); + } + } + + go(); + }); + } + + describe( 'fs.watch', () => { + runTests( false ); + }); + + if ( !process.env.CI ) { + describe( 'chokidar', () => { + runTests( true ); + }); + } + + function runTests ( chokidar ) { + it( 'watches a file', () => { + return sander.copydir( 'test/watch/samples/basic' ).to( 'test/_tmp/input' ).then( () => { + const watcher = rollup.watch({ + entry: 'test/_tmp/input/main.js', + dest: 'test/_tmp/output/bundle.js', + format: 'cjs', + watch: { chokidar } + }); + + return sequence( watcher, [ + 'BUILD_START', + 'BUILD_END', + () => { + assert.equal( run( './_tmp/output/bundle.js' ), 42 ); + sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); + }, + 'BUILD_START', + 'BUILD_END', + () => { + assert.equal( run( './_tmp/output/bundle.js' ), 43 ); + watcher.close(); + } + ]); + }); + }); + + it( 'recovers from an error', () => { + return sander.copydir( 'test/watch/samples/basic' ).to( 'test/_tmp/input' ).then( () => { + const watcher = rollup.watch({ + entry: 'test/_tmp/input/main.js', + dest: 'test/_tmp/output/bundle.js', + format: 'cjs', + watch: { chokidar } + }); + + return sequence( watcher, [ + 'BUILD_START', + 'BUILD_END', + () => { + assert.equal( run( './_tmp/output/bundle.js' ), 42 ); + sander.writeFileSync( 'test/_tmp/input/main.js', 'export nope;' ); + }, + 'BUILD_START', + 'ERROR', + () => { + sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); + }, + 'BUILD_START', + 'BUILD_END', + () => { + assert.equal( run( './_tmp/output/bundle.js' ), 43 ); + watcher.close(); + } + ]); + }); + }); + + it( 'recovers from an error even when erroring file was "renamed" (#38)', () => { + return sander.copydir( 'test/watch/samples/basic' ).to( 'test/_tmp/input' ).then( () => { + const watcher = rollup.watch({ + entry: 'test/_tmp/input/main.js', + dest: 'test/_tmp/output/bundle.js', + format: 'cjs', + watch: { chokidar } + }); + + return sequence( watcher, [ + 'BUILD_START', + 'BUILD_END', + () => { + assert.equal( run( './_tmp/output/bundle.js' ), 42 ); + sander.unlinkSync( 'test/_tmp/input/main.js' ); + sander.writeFileSync( 'test/_tmp/input/main.js', 'export nope;' ); + }, + 'BUILD_START', + 'ERROR', + () => { + sander.unlinkSync( 'test/_tmp/input/main.js' ); + sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); + }, + 'BUILD_START', + 'BUILD_END', + () => { + assert.equal( run( './_tmp/output/bundle.js' ), 43 ); + watcher.close(); + } + ]); + }); + }); + + it( 'refuses to watch the output file (#15)', () => { + return sander.copydir( 'test/watch/samples/basic' ).to( 'test/_tmp/input' ).then( () => { + const watcher = rollup.watch({ + entry: 'test/_tmp/input/main.js', + dest: 'test/_tmp/output/bundle.js', + format: 'cjs', + watch: { chokidar } + }); + + return sequence( watcher, [ + 'BUILD_START', + 'BUILD_END', + () => { + assert.equal( run( './_tmp/output/bundle.js' ), 42 ); + sander.writeFileSync( 'test/_tmp/input/main.js', `import '../output/bundle.js'` ); + }, + 'BUILD_START', + 'ERROR', + event => { + assert.equal( event.error.message, 'Cannot import the generated bundle' ); + sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); + }, + 'BUILD_START', + 'BUILD_END', + () => { + assert.equal( run( './_tmp/output/bundle.js' ), 43 ); + watcher.close(); + } + ]); + }); + }); + + it( 'ignores files that are not specified in options.watch.include, if given', () => { + return sander.copydir( 'test/watch/samples/ignored' ).to( 'test/_tmp/input' ).then( () => { + const watcher = rollup.watch({ + entry: 'test/_tmp/input/main.js', + dest: 'test/_tmp/output/bundle.js', + format: 'cjs', + watch: { + chokidar, + include: ['test/_tmp/input/+(main|foo).js'] + } + }); + + return sequence( watcher, [ + 'BUILD_START', + 'BUILD_END', + () => { + assert.deepEqual( run( './_tmp/output/bundle.js' ), { foo: 'foo-1', bar: 'bar-1' }); + sander.writeFileSync( 'test/_tmp/input/foo.js', `export default 'foo-2';` ); + }, + 'BUILD_START', + 'BUILD_END', + () => { + assert.deepEqual( run( './_tmp/output/bundle.js' ), { foo: 'foo-2', bar: 'bar-1' }); + sander.writeFileSync( 'test/_tmp/input/bar.js', `export default 'bar-2';` ); + }, + () => { + assert.deepEqual( run( './_tmp/output/bundle.js' ), { foo: 'foo-2', bar: 'bar-1' }); + watcher.close(); + } + ]); + }); + }); + + it( 'ignores files that are specified in options.watch.exclude, if given', () => { + return sander.copydir( 'test/watch/samples/ignored' ).to( 'test/_tmp/input' ).then( () => { + const watcher = rollup.watch({ + entry: 'test/_tmp/input/main.js', + dest: 'test/_tmp/output/bundle.js', + format: 'cjs', + watch: { + chokidar, + exclude: ['test/_tmp/input/bar.js'] + } + }); + + return sequence( watcher, [ + 'BUILD_START', + 'BUILD_END', + () => { + assert.deepEqual( run( './_tmp/output/bundle.js' ), { foo: 'foo-1', bar: 'bar-1' }); + sander.writeFileSync( 'test/_tmp/input/foo.js', `export default 'foo-2';` ); + }, + 'BUILD_START', + 'BUILD_END', + () => { + assert.deepEqual( run( './_tmp/output/bundle.js' ), { foo: 'foo-2', bar: 'bar-1' }); + sander.writeFileSync( 'test/_tmp/input/bar.js', `export default 'bar-2';` ); + }, + () => { + assert.deepEqual( run( './_tmp/output/bundle.js' ), { foo: 'foo-2', bar: 'bar-1' }); + watcher.close(); + } + ]); + }); + }); + } + }); + }); diff --git a/test/watch/samples/basic/main.js b/test/watch/samples/basic/main.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/watch/samples/basic/main.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/watch/samples/ignored/bar.js b/test/watch/samples/ignored/bar.js new file mode 100644 index 00000000000..e36cec404c9 --- /dev/null +++ b/test/watch/samples/ignored/bar.js @@ -0,0 +1 @@ +export default 'bar-1'; \ No newline at end of file diff --git a/test/watch/samples/ignored/foo.js b/test/watch/samples/ignored/foo.js new file mode 100644 index 00000000000..8402d2657cb --- /dev/null +++ b/test/watch/samples/ignored/foo.js @@ -0,0 +1 @@ +export default 'foo-1'; \ No newline at end of file diff --git a/test/watch/samples/ignored/main.js b/test/watch/samples/ignored/main.js new file mode 100644 index 00000000000..d42f9080e17 --- /dev/null +++ b/test/watch/samples/ignored/main.js @@ -0,0 +1,4 @@ +import foo from './foo.js'; +import bar from './bar.js'; + +export { foo, bar }; \ No newline at end of file From f4d9765c381b5d5c20d1336bd9724112459defe6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Aug 2017 19:31:11 -0400 Subject: [PATCH 6/8] use rollup.watch in CLI, update tests, various other things --- .eslintrc => .eslintrc.json | 1 - bin/src/logging.js | 39 ++-- bin/src/run/batchWarnings.js | 212 +++++++++++------ bin/src/run/build.js | 50 ++-- bin/src/run/createWatcher.js | 217 ------------------ bin/src/run/index.js | 29 ++- bin/src/run/mergeOptions.js | 21 +- bin/src/run/watch.js | 88 ++++--- package-lock.json | 31 +++ package.json | 1 + rollup.config.js | 7 +- src/Bundle.js | 16 +- src/Module.js | 1 + src/ast/scopes/ModuleScope.js | 2 + src/finalisers/shared/getGlobalNameMaker.js | 2 + src/finalisers/shared/warnOnBuiltins.js | 3 +- src/rollup/index.js | 8 +- src/utils/collapseSourcemaps.js | 1 + src/utils/error.js | 3 +- src/utils/transform.js | 24 +- src/watch/fileWatchers.js | 23 +- src/watch/index.js | 144 +++++++----- .../namespace-missing-export/_config.js | 3 + test/function/unused-import/_config.js | 2 + test/function/warn-on-eval/_config.js | 3 + .../warn-on-namespace-conflict/_config.js | 8 + .../warn-on-top-level-this/_config.js | 2 + .../warn-on-unused-missing-imports/_config.js | 3 + test/test.js | 115 ++++++---- 29 files changed, 529 insertions(+), 530 deletions(-) rename .eslintrc => .eslintrc.json (95%) delete mode 100644 bin/src/run/createWatcher.js diff --git a/.eslintrc b/.eslintrc.json similarity index 95% rename from .eslintrc rename to .eslintrc.json index d9150f9de08..e35395ba7e3 100644 --- a/.eslintrc +++ b/.eslintrc.json @@ -5,7 +5,6 @@ "semi": [ 2, "always" ], "keyword-spacing": [ 2, { "before": true, "after": true } ], "space-before-blocks": [ 2, "always" ], - "space-before-function-paren": [ 2, "always" ], "no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ], "no-cond-assign": 0, "no-unused-vars": 2, diff --git a/bin/src/logging.js b/bin/src/logging.js index 69d336e0a20..0207c137732 100644 --- a/bin/src/logging.js +++ b/bin/src/logging.js @@ -2,42 +2,35 @@ import chalk from 'chalk'; import relativeId from '../../src/utils/relativeId.js'; if ( !process.stderr.isTTY ) chalk.enabled = false; -const warnSymbol = process.stderr.isTTY ? `⚠️ ` : `Warning: `; -const errorSymbol = process.stderr.isTTY ? `🚨 ` : `Error: `; // log to stderr to keep `rollup main.js > bundle.js` from breaking export const stderr = console.error.bind( console ); // eslint-disable-line no-console -function log ( object, symbol ) { - let description = object.message || object; - if (object.name) description = object.name + ': ' + description; - const message = (object.plugin ? `(${object.plugin} plugin) ${description}` : description) || object;; +export function handleError ( err, recover ) { + let description = err.message || err; + if (err.name) description = `${err.name}: ${description}`; + const message = (err.plugin ? `(${err.plugin} plugin) ${description}` : description) || err; - stderr( `${symbol}${chalk.bold( message )}` ); + stderr( chalk.bold.red( `[!] ${chalk.bold( message )}` ) ); - // TODO should this be "object.url || (object.file && object.loc.file) || object.id"? - if ( object.url ) { - stderr( chalk.cyan( object.url ) ); + // TODO should this be "err.url || (err.file && err.loc.file) || err.id"? + if ( err.url ) { + stderr( chalk.cyan( err.url ) ); } - if ( object.loc ) { - stderr( `${relativeId( object.loc.file || object.id )} (${object.loc.line}:${object.loc.column})` ); - } else if ( object.id ) { - stderr( relativeId( object.id ) ); + if ( err.loc ) { + stderr( `${relativeId( err.loc.file || err.id )} (${err.loc.line}:${err.loc.column})` ); + } else if ( err.id ) { + stderr( relativeId( err.id ) ); } - if ( object.frame ) { - stderr( chalk.dim( object.frame ) ); + if ( err.frame ) { + stderr( chalk.dim( err.frame ) ); + } else if ( err.stack ) { + stderr( chalk.dim( err.stack ) ); } stderr( '' ); -} -export function handleWarning ( warning ) { - log( warning, warnSymbol ); -} - -export function handleError ( err, recover ) { - log( err, errorSymbol ); if ( !recover ) process.exit( 1 ); } diff --git a/bin/src/run/batchWarnings.js b/bin/src/run/batchWarnings.js index 8ed81e5dada..1fbb69d9d5a 100644 --- a/bin/src/run/batchWarnings.js +++ b/bin/src/run/batchWarnings.js @@ -1,5 +1,5 @@ import chalk from 'chalk'; -import { handleWarning, stderr } from '../logging.js'; +import { stderr } from '../logging.js'; import relativeId from '../../../src/utils/relativeId.js'; export default function batchWarnings () { @@ -7,11 +7,20 @@ export default function batchWarnings () { let count = 0; return { + get count() { + return count; + }, + add: warning => { if ( typeof warning === 'string' ) { warning = { code: 'UNKNOWN', message: warning }; } + if ( warning.code in immediateHandlers ) { + immediateHandlers[ warning.code ]( warning ); + return; + } + if ( !allWarnings.has( warning.code ) ) allWarnings.set( warning.code, [] ); allWarnings.get( warning.code ).push( warning ); @@ -23,24 +32,37 @@ export default function batchWarnings () { const codes = Array.from( allWarnings.keys() ) .sort( ( a, b ) => { - if ( handlers[a] && handlers[b] ) { - return handlers[a].priority - handlers[b].priority; + if ( deferredHandlers[a] && deferredHandlers[b] ) { + return deferredHandlers[a].priority - deferredHandlers[b].priority; } - if ( handlers[a] ) return -1; - if ( handlers[b] ) return 1; + if ( deferredHandlers[a] ) return -1; + if ( deferredHandlers[b] ) return 1; return allWarnings.get( b ).length - allWarnings.get( a ).length; }); codes.forEach( code => { - const handler = handlers[ code ]; + const handler = deferredHandlers[ code ]; const warnings = allWarnings.get( code ); if ( handler ) { handler.fn( warnings ); } else { warnings.forEach( warning => { - handleWarning( warning ); + stderr( `${chalk.bold.yellow('(!)')} ${chalk.bold.yellow( warning.message )}` ); + + if ( warning.url ) info( warning.url ); + + const id = warning.loc && warning.loc.file || warning.id; + if ( id ) { + const loc = warning.loc ? + `${relativeId( id )}: (${warning.loc.line}:${warning.loc.column})` : + relativeId( id ); + + stderr( chalk.bold( relativeId( loc ) ) ); + } + + if ( warning.frame ) info( warning.frame ); }); } }); @@ -50,14 +72,34 @@ export default function batchWarnings () { }; } +const immediateHandlers = { + MISSING_NODE_BUILTINS: warning => { + title( `Missing shims for Node.js built-ins` ); + + const detail = warning.modules.length === 1 ? + `'${warning.modules[0]}'` : + `${warning.modules.slice( 0, -1 ).map( name => `'${name}'` ).join( ', ' )} and '${warning.modules.slice( -1 )}'`; + stderr( `Creating a browser bundle that depends on ${detail}. You might need to include https://www.npmjs.com/package/rollup-plugin-node-builtins` ); + }, + + MIXED_EXPORTS: () => { + title( 'Mixing named and default exports' ); + stderr( `Consumers of your bundle will have to use bundle['default'] to access the default export, which may not be what you want. Use \`exports: 'named'\` to disable this warning` ); + }, + + EMPTY_BUNDLE: () => { + title( `Generated an empty bundle` ); + } +}; + // TODO select sensible priorities -const handlers = { +const deferredHandlers = { UNUSED_EXTERNAL_IMPORT: { priority: 1, fn: warnings => { - group( 'Unused external imports' ); + title( 'Unused external imports' ); warnings.forEach( warning => { - stderr( `${warning.message}` ); + stderr( `${warning.names} imported from external module '${warning.source}' but never used` ); }); } }, @@ -65,7 +107,7 @@ const handlers = { UNRESOLVED_IMPORT: { priority: 1, fn: warnings => { - group( 'Unresolved dependencies' ); + title( 'Unresolved dependencies' ); info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency' ); const dependencies = new Map(); @@ -84,7 +126,7 @@ const handlers = { MISSING_EXPORT: { priority: 1, fn: warnings => { - group( 'Missing exports' ); + title( 'Missing exports' ); info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module' ); warnings.forEach( warning => { @@ -98,110 +140,136 @@ const handlers = { THIS_IS_UNDEFINED: { priority: 1, fn: warnings => { - group( '`this` has been rewritten to `undefined`' ); + title( '`this` has been rewritten to `undefined`' ); info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined' ); - - const modules = new Map(); - warnings.forEach( warning => { - if ( !modules.has( warning.loc.file ) ) modules.set( warning.loc.file, [] ); - modules.get( warning.loc.file ).push( warning ); - }); - - const allIds = Array.from( modules.keys() ); - const ids = allIds.length > 5 ? allIds.slice( 0, 3 ) : allIds; - - ids.forEach( id => { - const occurrences = modules.get( id ); - - stderr( chalk.bold( relativeId( id ) ) ); - stderr( chalk.grey( occurrences[0].frame ) ); - - if ( occurrences.length > 1 ) { - stderr( `...and ${occurrences.length - 1} other ${occurrences.length > 2 ? 'occurrences' : 'occurrence'}` ); - } - }); - - if ( allIds.length > ids.length ) { - stderr( `\n...and ${allIds.length - ids.length} other files` ); - } + showTruncatedWarnings(warnings); } }, EVAL: { priority: 1, fn: warnings => { - + title( 'Use of eval is strongly discouraged' ); + info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval' ); + showTruncatedWarnings(warnings); } }, NON_EXISTENT_EXPORT: { priority: 1, fn: warnings => { - + title( `Import of non-existent ${warnings.length > 1 ? 'exports' : 'export'}` ); + showTruncatedWarnings(warnings); } }, NAMESPACE_CONFLICT: { priority: 1, fn: warnings => { - + title( `Conflicting re-exports` ); + warnings.forEach(warning => { + stderr( `${chalk.bold(relativeId(warning.reexporter))} re-exports '${warning.name}' from both ${relativeId(warning.sources[0])} and ${relativeId(warning.sources[1])} (will be ignored)` ); + }); } }, - DEPRECATED_ES6: { + MISSING_GLOBAL_NAME: { priority: 1, fn: warnings => { - + title( `Missing global variable ${warnings.length > 1 ? 'names' : 'name'}` ); + stderr( `Use options.globals to specify browser global variable names corresponding to external modules` ); + warnings.forEach(warning => { + stderr(`${chalk.bold(warning.source)} (guessing '${warning.guess}')`); + }); } }, - EMPTY_BUNDLE: { + SOURCEMAP_BROKEN: { priority: 1, fn: warnings => { - - } - }, + title( `Broken sourcemap` ); + info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#sourcemap-is-likely-to-be-incorrect' ); - MISSING_GLOBAL_NAME: { - priority: 1, - fn: warnings => { - - } - }, + const plugins = Array.from( new Set( warnings.map( w => w.plugin ).filter( Boolean ) ) ); + const detail = plugins.length === 0 ? '' : plugins.length > 1 ? + ` (such as ${plugins.slice(0, -1).map(p => `'${p}'`).join(', ')} and '${plugins.slice(-1)}')` : + ` (such as '${plugins[0]}')`; - MISSING_NODE_BUILTINS: { - priority: 1, - fn: warnings => { - + stderr( `Plugins that transform code${detail} should generate accompanying sourcemaps` ); } }, - MISSING_FORMAT: { + PLUGIN_WARNING: { priority: 1, fn: warnings => { - - } - }, // TODO make this an error + const nestedByPlugin = nest(warnings, 'plugin'); - SOURCEMAP_BROKEN: { - priority: 1, - fn: warnings => { - - } - }, + nestedByPlugin.forEach(({ key: plugin, items }) => { + const nestedByMessage = nest(items, 'message'); - MIXED_EXPORTS: { - priority: 1, - fn: warnings => { - + let lastUrl; + + nestedByMessage.forEach(({ key: message, items }) => { + title( `${plugin} plugin: ${message}` ); + items.forEach(warning => { + if ( warning.url !== lastUrl ) info( lastUrl = warning.url ); + + const loc = warning.loc ? + `${relativeId( warning.id )}: (${warning.loc.line}:${warning.loc.column})` : + relativeId( warning.id ); + + stderr( chalk.bold( relativeId( loc ) ) ); + if ( warning.frame ) info( warning.frame ); + }); + }); + }); } } }; -function group ( title ) { - stderr( `${chalk.bold.yellow('(!)')} ${chalk.bold.yellow( title )}` ); +function title ( str ) { + stderr( `${chalk.bold.yellow('(!)')} ${chalk.bold.yellow( str )}` ); } function info ( url ) { stderr( chalk.grey( url ) ); +} + +function nest(array, prop) { + const nested = []; + const lookup = new Map(); + + array.forEach(item => { + const key = item[prop]; + if (!lookup.has(key)) { + lookup.set(key, { + key, + items: [] + }); + + nested.push(lookup.get(key)); + } + + lookup.get(key).items.push(item); + }); + + return nested; +} + +function showTruncatedWarnings(warnings) { + const nestedByModule = nest(warnings, 'id'); + + const sliced = nestedByModule.length > 5 ? nestedByModule.slice(0, 3) : nestedByModule; + sliced.forEach(({ key: id, items }) => { + stderr( chalk.bold( relativeId( id ) ) ); + stderr( chalk.grey( items[0].frame ) ); + + if ( items.length > 1 ) { + stderr( `...and ${items.length - 1} other ${items.length > 2 ? 'occurrences' : 'occurrence'}` ); + } + }); + + if ( nestedByModule.length > sliced.length ) { + stderr( `\n...and ${nestedByModule.length - sliced.length} other files` ); + } } \ No newline at end of file diff --git a/bin/src/run/build.js b/bin/src/run/build.js index 055bdae386b..0ef94446991 100644 --- a/bin/src/run/build.js +++ b/bin/src/run/build.js @@ -1,47 +1,45 @@ import * as rollup from 'rollup'; import chalk from 'chalk'; +import ms from 'pretty-ms'; import { handleError, stderr } from '../logging.js'; +import relativeId from '../../../src/utils/relativeId.js'; +import { mapSequence } from '../../../src/utils/promise.js'; import SOURCEMAPPING_URL from '../sourceMappingUrl.js'; -export default function build ( options, warnings ) { +export default function build ( options, warnings, silent ) { + const useStdout = !options.targets && !options.dest; + const targets = options.targets ? options.targets : [{ dest: options.dest, format: options.format }]; + const start = Date.now(); - stderr( chalk.green( `\n${chalk.bold( options.entry )} → ${chalk.bold( options.dest )}...` ) ); + const dests = useStdout ? [ 'stdout' ] : targets.map( t => relativeId( t.dest ) ); + if ( !silent ) stderr( chalk.cyan( `\n${chalk.bold( options.entry )} → ${chalk.bold( dests.join( ', ' ) )}...` ) ); return rollup.rollup( options ) .then( bundle => { - if ( options.dest ) { - return bundle.write( options ); - } - - if ( options.targets ) { - let result = null; - - options.targets.forEach( target => { - result = bundle.write( assign( clone( options ), target ) ); - }); + if ( useStdout ) { + if ( options.sourceMap && options.sourceMap !== 'inline' ) { + handleError({ + code: 'MISSING_OUTPUT_OPTION', + message: 'You must specify an --output (-o) option when creating a file with a sourcemap' + }); + } - return result; - } + return bundle.generate(options).then( ({ code, map }) => { + if ( options.sourceMap === 'inline' ) { + code += `\n//# ${SOURCEMAPPING_URL}=${map.toUrl()}\n`; + } - if ( options.sourceMap && options.sourceMap !== 'inline' ) { - handleError({ - code: 'MISSING_OUTPUT_OPTION', - message: 'You must specify an --output (-o) option when creating a file with a sourcemap' + process.stdout.write( code ); }); } - return bundle.generate(options).then( ({ code, map }) => { - if ( options.sourceMap === 'inline' ) { - code += `\n//# ${SOURCEMAPPING_URL}=${map.toUrl()}\n`; - } - - process.stdout.write( code ); + return mapSequence( targets, target => { + return bundle.write( assign( clone( options ), target ) ); }); }) .then( () => { warnings.flush(); - stderr( chalk.green( `${chalk.bold( options.dest )} created in ${Date.now() - start}ms\n` ) ); - // stderr( `${chalk.blue( '----------' )}\n` ); + if ( !silent ) stderr( chalk.green( `created ${chalk.bold( dests.join( ', ' ) )} in ${chalk.bold(ms( Date.now() - start))}` ) ); }) .catch( handleError ); } diff --git a/bin/src/run/createWatcher.js b/bin/src/run/createWatcher.js deleted file mode 100644 index 4ec02fa5ff0..00000000000 --- a/bin/src/run/createWatcher.js +++ /dev/null @@ -1,217 +0,0 @@ -import EventEmitter from 'events'; -import relative from 'require-relative'; -import path from 'path'; -import * as fs from 'fs'; -import createFilter from 'rollup-pluginutils/src/createFilter.js'; -import sequence from '../utils/sequence.js'; - -const opts = { encoding: 'utf-8', persistent: true }; - -let chokidar; - -try { - chokidar = relative( 'chokidar', process.cwd() ); -} catch (err) { - chokidar = null; -} - -class FileWatcher { - constructor ( file, data, callback, chokidarOptions, dispose ) { - const handleWatchEvent = (event) => { - if ( event === 'rename' || event === 'unlink' ) { - this.fsWatcher.close(); - dispose(); - callback(); - } else { - // this is necessary because we get duplicate events... - const contents = fs.readFileSync( file, 'utf-8' ); - if ( contents !== data ) { - data = contents; - callback(); - } - } - }; - - try { - if (chokidarOptions) { - this.fsWatcher = chokidar.watch(file, chokidarOptions).on('all', handleWatchEvent); - } else { - this.fsWatcher = fs.watch( file, opts, handleWatchEvent); - } - - this.fileExists = true; - } catch ( err ) { - if ( err.code === 'ENOENT' ) { - // can't watch files that don't exist (e.g. injected - // by plugins somehow) - this.fileExists = false; - } else { - throw err; - } - } - } - - close () { - this.fsWatcher.close(); - } -} - -export default function watch ( rollup, options ) { - const watchOptions = options.watch || {}; - - if ( 'useChokidar' in watchOptions ) watchOptions.chokidar = watchOptions.useChokidar; - let chokidarOptions = 'chokidar' in watchOptions ? watchOptions.chokidar : !!chokidar; - if ( chokidarOptions ) { - chokidarOptions = Object.assign( chokidarOptions === true ? {} : chokidarOptions, { - ignoreInitial: true - }); - } - - if ( chokidarOptions && !chokidar ) { - throw new Error( `options.watch.chokidar was provided, but chokidar could not be found. Have you installed it?` ); - } - - const watcher = new EventEmitter(); - - const filter = createFilter( watchOptions.include, watchOptions.exclude ); - const dests = options.dest ? [ path.resolve( options.dest ) ] : options.targets.map( target => path.resolve( target.dest ) ); - let filewatchers = new Map(); - - let rebuildScheduled = false; - let building = false; - let watching = false; - let closed = false; - - let timeout; - let cache; - - function triggerRebuild () { - clearTimeout( timeout ); - rebuildScheduled = true; - - timeout = setTimeout( () => { - if ( !building ) build(); - }, 50 ); - } - - function addFileWatchersForModules ( modules ) { - modules.forEach( module => { - let id = module.id; - - // skip plugin helper modules and unwatched files - if ( /\0/.test( id ) ) return; - if ( !filter( id ) ) return; - - try { - id = fs.realpathSync( id ); - } catch ( err ) { - return; - } - - if ( ~dests.indexOf( id ) ) { - throw new Error( 'Cannot import the generated bundle' ); - } - - if ( !filewatchers.has( id ) ) { - const watcher = new FileWatcher( id, module.originalCode, triggerRebuild, chokidarOptions, () => { - filewatchers.delete( id ); - }); - - if ( watcher.fileExists ) filewatchers.set( id, watcher ); - } - }); - } - - function build () { - if ( building || closed ) return; - - rebuildScheduled = false; - - let start = Date.now(); - let initial = !watching; - if ( cache ) options.cache = cache; - - watcher.emit( 'event', { code: 'BUILD_START' }); - - building = true; - - return rollup.rollup( options ) - .then( bundle => { - // Save off bundle for re-use later - cache = bundle; - - if ( !closed ) { - addFileWatchersForModules(bundle.modules); - } - - // Now we're watching - watching = true; - - if ( options.targets ) { - return sequence( options.targets, target => { - const mergedOptions = Object.assign( {}, options, target ); - return bundle.write( mergedOptions ); - }); - } - - return bundle.write( options ); - }) - .then( () => { - watcher.emit( 'event', { - code: 'BUILD_END', - duration: Date.now() - start, - initial - }); - }, error => { - try { - //If build failed, make sure we are still watching those files from the most recent successful build. - addFileWatchersForModules( cache.modules ); - } - catch (e) { - //Ignore if they tried to import the output. We are already inside of a catch (probably caused by that). - } - watcher.emit( 'event', { - code: 'ERROR', - error - }); - }) - .then( () => { - building = false; - if ( rebuildScheduled && !closed ) build(); - }); - } - - // build on next tick, so consumers can listen for BUILD_START - process.nextTick( build ); - - function close () { - if ( closed ) return; - for ( const fw of filewatchers.values() ) { - fw.close(); - } - - process.removeListener('SIGINT', close); - process.removeListener('SIGTERM', close); - process.removeListener('uncaughtException', close); - process.stdin.removeListener('end', close); - - watcher.removeAllListeners(); - closed = true; - } - - watcher.close = close; - - // ctrl-c - process.on('SIGINT', close); - - // killall node - process.on('SIGTERM', close); - - // on error - process.on('uncaughtException', close); - - // in case we ever support stdin! - process.stdin.on('end', close); - - return watcher; -} diff --git a/bin/src/run/index.js b/bin/src/run/index.js index 06ac892c806..5091e9fcef2 100644 --- a/bin/src/run/index.js +++ b/bin/src/run/index.js @@ -1,10 +1,12 @@ import path from 'path'; +import chalk from 'chalk'; import { realpathSync } from 'fs'; import * as rollup from 'rollup'; import relative from 'require-relative'; -import { handleWarning, handleError } from '../logging.js'; +import { handleError, stderr } from '../logging.js'; import mergeOptions from './mergeOptions.js'; import batchWarnings from './batchWarnings.js'; +import relativeId from '../../../src/utils/relativeId.js'; import sequence from '../utils/sequence.js'; import build from './build.js'; import watch from './watch.js'; @@ -78,7 +80,10 @@ export default function runRollup ( command ) { onwarn: warnings.add }) .then( bundle => { - warnings.flush(); + if ( !command.silent && warnings.count > 0 ) { + stderr( chalk.bold( `loaded ${relativeId( config )} with warnings` ) ); + warnings.flush(); + } return bundle.generate({ format: 'cjs' @@ -118,11 +123,23 @@ export default function runRollup ( command ) { function execute ( configs, command ) { if ( command.watch ) { process.env.ROLLUP_WATCH = 'true'; - watch( configs, command ); + watch( configs, command, command.silent ); } else { - return sequence( config => { - const { options, warnings } = mergeOptions( config, command ); - return build( options, warnings ); + return sequence( configs, config => { + const options = mergeOptions( config, command ); + + const warnings = batchWarnings(); + + const onwarn = options.onwarn; + if ( onwarn ) { + options.onwarn = warning => { + onwarn( warning, warnings.add ); + }; + } else { + options.onwarn = warnings.add; + } + + return build( options, warnings, command.silent ); }); } } \ No newline at end of file diff --git a/bin/src/run/mergeOptions.js b/bin/src/run/mergeOptions.js index c56e08e98f8..8b12514258f 100644 --- a/bin/src/run/mergeOptions.js +++ b/bin/src/run/mergeOptions.js @@ -54,21 +54,8 @@ export default function mergeOptions ( config, command ) { options.extend = command.extend; } - const warnings = batchWarnings(); - - if ( command.silent ) { + if (command.silent) { options.onwarn = () => {}; - } else { - options.onwarn = warnings.add; - - const onwarn = options.onwarn; - if ( onwarn ) { - options.onwarn = warning => { - onwarn( warning, warnings.add ); - }; - } else { - options.onwarn = warnings.add; - } } options.external = external; @@ -80,5 +67,9 @@ export default function mergeOptions ( config, command ) { } }); - return { options, warnings }; + const targets = options.dest ? [{ dest: options.dest, format: options.format }] : options.targets; + options.targets = targets; + delete options.dest; + + return options; } \ No newline at end of file diff --git a/bin/src/run/watch.js b/bin/src/run/watch.js index 9bd5ff90f33..751e98c83ab 100644 --- a/bin/src/run/watch.js +++ b/bin/src/run/watch.js @@ -1,44 +1,74 @@ import * as rollup from 'rollup'; import chalk from 'chalk'; -import createWatcher from './createWatcher.js'; +import ms from 'pretty-ms'; import mergeOptions from './mergeOptions.js'; +import batchWarnings from './batchWarnings.js'; +import relativeId from '../../../src/utils/relativeId.js'; import { handleError, stderr } from '../logging.js'; -export default function watch ( configs, command ) { - configs.forEach( config => { - const { options, warnings } = mergeOptions( config, command ); +export default function watch(configs, command, silent) { + process.stderr.write('\x1b[?1049h'); // alternate screen buffer - if ( !options.entry || ( !options.dest && !options.targets ) ) { - handleError({ - code: 'WATCHER_MISSING_INPUT_OR_OUTPUT', - message: 'must specify --input and --output when using rollup --watch' - }); + const warnings = batchWarnings(); + + configs = configs.map(options => { + const merged = mergeOptions(options, command); + + const onwarn = merged.onwarn; + if ( onwarn ) { + merged.onwarn = warning => { + onwarn( warning, warnings.add ); + }; + } else { + merged.onwarn = warnings.add; } - const watcher = createWatcher( rollup, options ); + return merged; + }); + + const watcher = rollup.watch(configs); - watcher.on( 'event', event => { - switch ( event.code ) { - case 'STARTING': // TODO this isn't emitted by newer versions of rollup-watch - stderr( 'checking rollup-watch version...' ); - break; + watcher.on('event', event => { + switch (event.code) { + case 'FATAL': + process.stderr.write('\x1b[?1049l'); // reset screen buffer + handleError(event.error, true); + process.exit(1); + break; - case 'BUILD_START': - stderr( `${chalk.blue.bold( options.entry )} -> ${chalk.blue.bold( options.dest )}...` ); - break; + case 'ERROR': + warnings.flush(); + handleError(event.error, true); + break; - case 'BUILD_END': - warnings.flush(); - stderr( `created ${chalk.blue.bold( options.dest )} in ${event.duration}ms. Watching for changes...` ); - break; + case 'START': + stderr(`\x1B[2J\x1B[0f${chalk.underline( 'rollup.watch' )}`); // clear, move to top-left + break; - case 'ERROR': - handleError( event.error, true ); - break; + case 'BUNDLE_START': + if ( !silent ) stderr( chalk.cyan( `\n${chalk.bold( event.input )} → ${chalk.bold( event.output.map( relativeId ).join( ', ' ) )}...` ) ); + break; - default: - stderr( 'unknown event', event ); - } - }); + case 'BUNDLE_END': + warnings.flush(); + if ( !silent ) stderr( chalk.green( `created ${chalk.bold( event.output.map( relativeId ).join( ', ' ) )} in ${chalk.bold(ms(event.duration))}` ) ); + break; + + case 'END': + if ( !silent ) stderr( `\nwaiting for changes...` ); + } }); + + let closed = false; + const close = () => { + if (!closed) { + process.stderr.write('\x1b[?1049l'); // reset screen buffer + closed = true; + watcher.close(); + } + }; + process.on('SIGINT', close); // ctrl-c + process.on('SIGTERM', close); // killall node + process.on('uncaughtException', close); // on error + process.stdin.on('end', close); // in case we ever support stdin! } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 500e3f2c595..20a9568c5fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2439,6 +2439,12 @@ "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", "dev": true }, + "irregular-plurals": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.3.0.tgz", + "integrity": "sha512-njf5A+Mxb3kojuHd1DzISjjIl+XhyzovXEOyPPSzdQozq/Lf2tN27mOrAAsxEPZxpn6I4MGzs1oo9TxXxPFpaA==", + "dev": true + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3399,6 +3405,12 @@ "error-ex": "1.3.1" } }, + "parse-ms": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz", + "integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0=", + "dev": true + }, "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", @@ -3465,6 +3477,15 @@ "find-up": "1.1.2" } }, + "plur": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", + "integrity": "sha1-dIJFLBoPUI4+NE6uwxLJHCncZVo=", + "dev": true, + "requires": { + "irregular-plurals": "1.3.0" + } + }, "pluralize": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", @@ -3483,6 +3504,16 @@ "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", "dev": true }, + "pretty-ms": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-3.0.0.tgz", + "integrity": "sha1-8cwgKLZvX6c2rCPvV3A9fmhKsQE=", + "dev": true, + "requires": { + "parse-ms": "1.0.1", + "plur": "2.1.2" + } + }, "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", diff --git a/package.json b/package.json index c797aa6346c..e22c47bcaff 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "magic-string": "^0.21.3", "minimist": "^1.2.0", "mocha": "^3.0.0", + "pretty-ms": "^3.0.0", "remap-istanbul": "^0.9.5", "require-relative": "^0.8.7", "rollup": "^0.42.0", diff --git a/rollup.config.js b/rollup.config.js index cc0b9ba4a65..9ae4776bdd6 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,6 +1,7 @@ import { readFileSync } from 'fs'; import buble from 'rollup-plugin-buble'; import nodeResolve from 'rollup-plugin-node-resolve'; +import commonjs from 'rollup-plugin-commonjs'; import replace from 'rollup-plugin-replace'; var pkg = JSON.parse( readFileSync( 'package.json', 'utf-8' ) ); @@ -36,8 +37,10 @@ export default { include: 'src/rollup.js', delimiters: [ '<@', '@>' ], sourceMap: true, - values: { 'VERSION': pkg.version } - }) + values: { VERSION: pkg.version } + }), + + commonjs() ], external: [ 'fs', diff --git a/src/Bundle.js b/src/Bundle.js index d6e9ace3b04..7cb6a5784c6 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -217,10 +217,12 @@ export default class Bundle { const names = unused.length === 1 ? `'${unused[0]}' is` : - `${unused.slice( 0, -1 ).map( name => `'${name}'` ).join( ', ' )} and '${unused.pop()}' are`; + `${unused.slice( 0, -1 ).map( name => `'${name}'` ).join( ', ' )} and '${unused.slice( -1 )}' are`; this.warn({ code: 'UNUSED_EXTERNAL_IMPORT', + source: module.id, + names: unused, message: `${names} imported from external module '${module.id}' but never used` }); }); @@ -356,6 +358,9 @@ export default class Bundle { if ( name in module.exportsAll ) { this.warn({ code: 'NAMESPACE_CONFLICT', + reexporter: module.id, + name, + sources: [ module.exportsAll[ name ], exportAllModule.exportsAll[ name ] ], message: `Conflicting namespaces: ${relativeId( module.id )} re-exports '${name}' from both ${relativeId( module.exportsAll[ name ] )} and ${relativeId( exportAllModule.exportsAll[ name ] )} (will be ignored)` }); } else { @@ -449,15 +454,6 @@ export default class Bundle { render ( options = {} ) { return Promise.resolve().then( () => { - if ( options.format === 'es6' ) { - this.warn({ - code: 'DEPRECATED_ES6', - message: 'The es6 format is deprecated – use `es` instead' - }); - - options.format = 'es'; - } - // Determine export mode - 'default', 'named', 'none' const exportMode = getExportMode( this, options ); diff --git a/src/Module.js b/src/Module.js index 9ba3f205dc3..72a88aa50ee 100644 --- a/src/Module.js +++ b/src/Module.js @@ -456,6 +456,7 @@ export default class Module { warning.frame = getCodeFrame( this.code, line, column ); } + warning.id = this.id; this.bundle.warn( warning ); } } diff --git a/src/ast/scopes/ModuleScope.js b/src/ast/scopes/ModuleScope.js index 9a1f9ee75a7..1da09aae483 100644 --- a/src/ast/scopes/ModuleScope.js +++ b/src/ast/scopes/ModuleScope.js @@ -29,6 +29,8 @@ export default class ModuleScope extends Scope { if ( !declaration ) { this.module.warn({ code: 'NON_EXISTENT_EXPORT', + name: specifier.name, + source: specifier.module.id, message: `Non-existent export '${specifier.name}' is imported from ${relativeId( specifier.module.id )}` }, specifier.specifier.start ); return; diff --git a/src/finalisers/shared/getGlobalNameMaker.js b/src/finalisers/shared/getGlobalNameMaker.js index ee3475653ed..aa208f5b745 100644 --- a/src/finalisers/shared/getGlobalNameMaker.js +++ b/src/finalisers/shared/getGlobalNameMaker.js @@ -8,6 +8,8 @@ export default function getGlobalNameMaker ( globals, bundle, fallback = null ) if ( Object.keys( module.declarations ).length > 0 ) { bundle.warn({ code: 'MISSING_GLOBAL_NAME', + source: module.id, + guess: module.name, message: `No name was provided for external module '${module.id}' in options.globals – guessing '${module.name}'` }); diff --git a/src/finalisers/shared/warnOnBuiltins.js b/src/finalisers/shared/warnOnBuiltins.js index c4d63f6f4de..f4aa878cc04 100644 --- a/src/finalisers/shared/warnOnBuiltins.js +++ b/src/finalisers/shared/warnOnBuiltins.js @@ -33,10 +33,11 @@ export default function warnOnBuiltins ( bundle ) { const detail = externalBuiltins.length === 1 ? `module ('${externalBuiltins[0]}')` : - `modules (${externalBuiltins.slice( 0, -1 ).map( name => `'${name}'` ).join( ', ' )} and '${externalBuiltins.pop()}')`; + `modules (${externalBuiltins.slice( 0, -1 ).map( name => `'${name}'` ).join( ', ' )} and '${externalBuiltins.slice( -1 )}')`; bundle.warn({ code: 'MISSING_NODE_BUILTINS', + modules: externalBuiltins, message: `Creating a browser bundle that depends on Node.js built-in ${detail}. You might need to include https://www.npmjs.com/package/rollup-plugin-node-builtins` }); } diff --git a/src/rollup/index.js b/src/rollup/index.js index 7213c1ea1f8..edc868d8fad 100644 --- a/src/rollup/index.js +++ b/src/rollup/index.js @@ -93,10 +93,14 @@ export default function rollup ( options ) { timeEnd( '--BUILD--' ); function generate ( options = {} ) { + if ( options.format === 'es6' ) { + throw new Error( 'The `es6` output format is deprecated – use `es` instead' ); + } + if ( !options.format ) { - bundle.warn({ // TODO make this an error + error({ // TODO make this an error code: 'MISSING_FORMAT', - message: `No format option was supplied – defaulting to 'es'`, + message: `You must supply an output format`, url: `https://github.com/rollup/rollup/wiki/JavaScript-API#format` }); diff --git a/src/utils/collapseSourcemaps.js b/src/utils/collapseSourcemaps.js index b77ef9def6a..db4cebb1c06 100644 --- a/src/utils/collapseSourcemaps.js +++ b/src/utils/collapseSourcemaps.js @@ -135,6 +135,7 @@ export default function collapseSourcemaps ( bundle, file, map, modules, bundleS if ( map.missing ) { bundle.warn({ code: 'SOURCEMAP_BROKEN', + plugin: map.plugin, message: `Sourcemap is likely to be incorrect: a plugin${map.plugin ? ` ('${map.plugin}')` : ``} was used to transform files, but didn't generate a sourcemap for the transformation. Consult the plugin documentation for help`, url: `https://github.com/rollup/rollup/wiki/Troubleshooting#sourcemap-is-likely-to-be-incorrect` }); diff --git a/src/utils/error.js b/src/utils/error.js index c7d46d07fe3..e5cd9768b25 100644 --- a/src/utils/error.js +++ b/src/utils/error.js @@ -4,8 +4,7 @@ export default function error ( props ) { // (Object.keys below does not update these values because they // are properties on the prototype chain) // basically if props is a SyntaxError it will not be overriden as a generic Error - let constructor = Error; - if (props instanceof Error) constructor = props.constructor; + const constructor = (props instanceof Error) ? props.constructor : Error; const err = new constructor( props.message ); Object.keys( props ).forEach( key => { diff --git a/src/utils/transform.js b/src/utils/transform.js index 0c0d9de5a71..b22acdec3a8 100644 --- a/src/utils/transform.js +++ b/src/utils/transform.js @@ -26,7 +26,8 @@ export default function transform ( bundle, source, id, plugins ) { object = { message: object }; } - if ( !object.code ) object.code = code; + if ( object.code ) object.pluginCode = object.code; + object.code = code; if ( pos !== undefined ) { if ( pos.line !== undefined && pos.column !== undefined ) { @@ -42,21 +43,24 @@ export default function transform ( bundle, source, id, plugins ) { } } + object.plugin = plugin.name; + object.id = id; + return object; } - let err; + let throwing; const context = { warn: ( warning, pos ) => { warning = augment( warning, pos, 'PLUGIN_WARNING' ); - warning.plugin = plugin.name; - warning.id = id; bundle.warn( warning ); }, - error ( e, pos ) { - err = augment( e, pos, 'PLUGIN_ERROR' ); + error ( err, pos ) { + err = augment( err, pos, 'PLUGIN_ERROR' ); + throwing = true; + error( err ); } }; @@ -65,13 +69,12 @@ export default function transform ( bundle, source, id, plugins ) { try { transformed = plugin.transform.call( context, previous, id ); } catch ( err ) { - context.error( err ); + if ( !throwing ) context.error( err ); + error( err ); } return Promise.resolve( transformed ) .then( result => { - if ( err ) throw err; - if ( result == null ) return previous; if ( typeof result === 'string' ) { @@ -97,8 +100,7 @@ export default function transform ( bundle, source, id, plugins ) { return result.code; }) .catch( err => { - err.plugin = plugin.name; - err.id = id; + err = augment( err, undefined, 'PLUGIN_ERROR' ); error( err ); }); }); diff --git a/src/watch/fileWatchers.js b/src/watch/fileWatchers.js index 0070f6572af..910bdaf7ffe 100644 --- a/src/watch/fileWatchers.js +++ b/src/watch/fileWatchers.js @@ -6,29 +6,34 @@ const opts = { encoding: 'utf-8', persistent: true }; const watchers = new Map(); -export function addTask(id, task) { - if (!watchers.has(id)) { - const watcher = new FileWatcher(id, () => { - watchers.delete(id); +export function addTask(id, task, chokidarOptions, chokidarOptionsHash) { + if (!watchers.has(chokidarOptionsHash)) watchers.set(chokidarOptionsHash, new Map()); + const group = watchers.get(chokidarOptionsHash); + + if (!group.has(id)) { + const watcher = new FileWatcher(id, chokidarOptions, () => { + group.delete(id); }); if (watcher.fileExists) { - watchers.set(id, watcher); + group.set(id, watcher); } else { return; } } - watchers.get(id).tasks.add(task); + group.get(id).tasks.add(task); } -export function deleteTask(id, target) { - const watcher = watchers.get(id); +export function deleteTask(id, target, chokidarOptionsHash) { + const group = watchers.get(chokidarOptionsHash); + + const watcher = group.get(id); watcher.tasks.delete(target); if (watcher.tasks.size === 0) { watcher.close(); - watchers.delete(id); + group.delete(id); } } diff --git a/src/watch/index.js b/src/watch/index.js index 52cb823e61a..58975948bc6 100644 --- a/src/watch/index.js +++ b/src/watch/index.js @@ -1,9 +1,11 @@ import path from 'path'; import EventEmitter from 'events'; +import createFilter from 'rollup-pluginutils/src/createFilter.js'; import rollup from '../rollup/index.js'; import ensureArray from '../utils/ensureArray.js'; import { mapSequence } from '../utils/promise.js'; import { addTask, deleteTask } from './fileWatchers.js'; +import chokidar from './chokidar.js'; const DELAY = 100; @@ -14,9 +16,10 @@ class Watcher extends EventEmitter { this.dirty = true; this.running = false; this.tasks = ensureArray(configs).map(config => new Task(this, config)); + this.succeeded = false; process.nextTick(() => { - this.run(); + this._run(); }); } @@ -26,125 +29,154 @@ class Watcher extends EventEmitter { }); } - error(error) { - this.emit('event', { - code: 'ERROR', - error - }); - } - - makeDirty() { + _makeDirty() { if (this.dirty) return; this.dirty = true; if (!this.running) { setTimeout(() => { - this.run(); + this._run(); }, DELAY); } } - run() { + _run() { this.running = true; this.dirty = false; - // TODO this.emit('event', { - code: 'BUILD_START' + code: 'START' }); mapSequence(this.tasks, task => { return task.run().catch(error => { this.emit('event', { - code: 'ERROR', + code: this.succeeded ? 'ERROR' : 'FATAL', error }); }); }).then(() => { this.running = false; + if (this.dirty) { + this._run(); + } else { + this.emit('event', { + code: 'END' + }); - this.emit('event', { - code: 'BUILD_END' - }); - - if (this.dirty) this.run(); + this.succeeded = true; + } }); } } class Task { - constructor(watcher, config) { + constructor(watcher, options) { this.cache = null; this.watcher = watcher; - this.config = config; + this.options = options; this.closed = false; this.watched = new Set(); - this.dests = new Set( - (config.dest ? [config.dest] : config.targets.map(t => t.dest)).map(dest => path.resolve(dest)) - ); + this.targets = options.targets ? options.targets : [{ dest: options.dest, format: options.format }]; + + this.dests = (this.targets.map(t => t.dest)).map(dest => path.resolve(dest)); + + const watchOptions = options.watch || {}; + if ('useChokidar' in watchOptions) watchOptions.chokidar = watchOptions.useChokidar; + let chokidarOptions = 'chokidar' in watchOptions ? watchOptions.chokidar : !!chokidar; + if (chokidarOptions) { + chokidarOptions = Object.assign( + chokidarOptions === true ? {} : chokidarOptions, + { + ignoreInitial: true + } + ); + } + + if (chokidarOptions && !chokidar) { + throw new Error(`options.watch.chokidar was provided, but chokidar could not be found. Have you installed it?`); + } + + this.chokidarOptions = chokidarOptions; + this.chokidarOptionsHash = JSON.stringify(chokidarOptions); + + this.filter = createFilter(watchOptions.include, watchOptions.exclude); } close() { this.closed = true; this.watched.forEach(id => { - deleteTask(id, this); + deleteTask(id, this, this.chokidarOptionsHash); }); } makeDirty() { if (!this.dirty) { this.dirty = true; - this.watcher.makeDirty(); + this.watcher._makeDirty(); } } run() { this.dirty = false; - const config = Object.assign(this.config, { + const options = Object.assign(this.options, { cache: this.cache }); - return rollup(config).then(bundle => { - if (this.closed) return; + const start = Date.now(); - this.cache = bundle; + this.watcher.emit('event', { + code: 'BUNDLE_START', + input: this.options.entry, + output: this.dests + }); - const watched = new Set(); + return rollup(options) + .then(bundle => { + if (this.closed) return; - bundle.modules.forEach(module => { - if (this.dests.has(module.id)) { - throw new Error('Cannot import the generated bundle'); - } + this.cache = bundle; - watched.add(module.id); - addTask(module.id, this); - }); + const watched = new Set(); - this.watched.forEach(id => { - if (!watched.has(id)) deleteTask(id, this); - }); + bundle.modules.forEach(module => { + if (!this.filter(module.id)) return; - this.watched = watched; + if (~this.dests.indexOf(module.id)) { + throw new Error('Cannot import the generated bundle'); + } - if (this.config.dest) { - return bundle.write({ - format: this.config.format, - dest: this.config.dest + watched.add(module.id); + addTask(module.id, this, this.chokidarOptions, this.chokidarOptionsHash); }); - } - return Promise.all( - this.config.targets.map(target => { - return bundle.write({ - format: target.format, - dest: target.dest - }); - }) - ); - }); + this.watched.forEach(id => { + if (!watched.has(id)) deleteTask(id, this, this.chokidarOptionsHash); + }); + + this.watched = watched; + + return Promise.all( + this.targets.map(target => { + return bundle.write({ + format: target.format, + dest: target.dest, + moduleName: this.options.moduleName + }); + }) + ); + }) + .then(() => { + this.watcher.emit('event', { + code: 'BUNDLE_END', + input: this.options.entry, + output: this.dests, + duration: Date.now() - start + }); + }); } } diff --git a/test/function/namespace-missing-export/_config.js b/test/function/namespace-missing-export/_config.js index 6848cc28686..92a7eaac809 100644 --- a/test/function/namespace-missing-export/_config.js +++ b/test/function/namespace-missing-export/_config.js @@ -1,9 +1,12 @@ +const path = require('path'); + module.exports = { warnings: [ { code: 'MISSING_EXPORT', exporter: 'empty.js', importer: 'main.js', + id: path.resolve( __dirname, 'main.js' ), missing: 'foo', message: `'foo' is not exported by 'empty.js'`, pos: 61, diff --git a/test/function/unused-import/_config.js b/test/function/unused-import/_config.js index a60eaf85acf..8b7820d5b41 100644 --- a/test/function/unused-import/_config.js +++ b/test/function/unused-import/_config.js @@ -12,6 +12,8 @@ module.exports = { }, { code: 'UNUSED_EXTERNAL_IMPORT', + source: 'external', + names: ['unused', 'notused', 'neverused'], message: `'unused', 'notused' and 'neverused' are imported from external module 'external' but never used` }, { diff --git a/test/function/warn-on-eval/_config.js b/test/function/warn-on-eval/_config.js index a959bceb30a..bb1f0b648f9 100644 --- a/test/function/warn-on-eval/_config.js +++ b/test/function/warn-on-eval/_config.js @@ -1,8 +1,11 @@ +const path = require('path'); + module.exports = { description: 'warns about use of eval', warnings: [ { code: 'EVAL', + id: path.resolve(__dirname, 'main.js'), message: `Use of eval is strongly discouraged, as it poses security risks and may cause issues with minification`, pos: 13, loc: { diff --git a/test/function/warn-on-namespace-conflict/_config.js b/test/function/warn-on-namespace-conflict/_config.js index 90d8fd73b19..e6d488dc515 100644 --- a/test/function/warn-on-namespace-conflict/_config.js +++ b/test/function/warn-on-namespace-conflict/_config.js @@ -1,8 +1,16 @@ +const path = require('path'); + module.exports = { description: 'warns on duplicate export * from', warnings: [ { code: 'NAMESPACE_CONFLICT', + name: 'foo', + reexporter: path.resolve(__dirname, 'main.js'), + sources: [ + path.resolve(__dirname, 'foo.js'), + path.resolve(__dirname, 'deep.js') + ], message: `Conflicting namespaces: main.js re-exports 'foo' from both foo.js and deep.js (will be ignored)` } ] diff --git a/test/function/warn-on-top-level-this/_config.js b/test/function/warn-on-top-level-this/_config.js index 4d9033e6670..02163ba4b8a 100644 --- a/test/function/warn-on-top-level-this/_config.js +++ b/test/function/warn-on-top-level-this/_config.js @@ -1,3 +1,4 @@ +const path = require( 'path' ); const assert = require( 'assert' ); module.exports = { @@ -5,6 +6,7 @@ module.exports = { warnings: [ { code: 'THIS_IS_UNDEFINED', + id: path.resolve(__dirname, 'main.js'), message: `The 'this' keyword is equivalent to 'undefined' at the top level of an ES module, and has been rewritten`, pos: 81, loc: { diff --git a/test/function/warn-on-unused-missing-imports/_config.js b/test/function/warn-on-unused-missing-imports/_config.js index 08cc6bac16f..15d06b8a043 100644 --- a/test/function/warn-on-unused-missing-imports/_config.js +++ b/test/function/warn-on-unused-missing-imports/_config.js @@ -6,6 +6,9 @@ module.exports = { warnings: [ { code: 'NON_EXISTENT_EXPORT', + id: path.resolve(__dirname, 'main.js'), + source: path.resolve(__dirname, 'foo.js'), + name: 'b', message: `Non-existent export 'b' is imported from foo.js`, pos: 12, loc: { diff --git a/test/test.js b/test/test.js index a1a071caece..24f787a71ac 100644 --- a/test/test.js +++ b/test/test.js @@ -168,7 +168,7 @@ describe( 'rollup', function () { }); }); - it( 'warns on missing format option', () => { + it( 'throws on missing format option', () => { const warnings = []; return rollup.rollup({ @@ -176,14 +176,9 @@ describe( 'rollup', function () { plugins: [ loader({ x: `console.log( 42 );` }) ], onwarn: warning => warnings.push( warning ) }).then( bundle => { - bundle.generate(); - compareWarnings( warnings, [ - { - code: 'MISSING_FORMAT', - message: `No format option was supplied – defaulting to 'es'`, - url: `https://github.com/rollup/rollup/wiki/JavaScript-API#format` - } - ]); + assert.throws(() => { + bundle.generate(); + }, /You must supply an output format/ ); }); }); }); @@ -261,9 +256,7 @@ describe( 'rollup', function () { }); }); - it( 'warns on es6 format', () => { - let warned; - + it( 'throws on es6 format', () => { return rollup.rollup({ entry: 'x', plugins: [{ @@ -271,14 +264,11 @@ describe( 'rollup', function () { load: () => { return '// empty'; } - }], - onwarn: msg => { - if ( /The es6 format is deprecated/.test( msg ) ) warned = true; - } + }] }).then( bundle => { - return bundle.generate({ format: 'es6' }); - }).then( () => { - assert.ok( warned ); + assert.throws(() => { + return bundle.generate({ format: 'es6' }); + }, /The `es6` output format is deprecated – use `es` instead/); }); }); }); @@ -864,7 +854,8 @@ describe( 'rollup', function () { ] }).then( bundle => { return bundle.write({ - dest + dest, + format: 'es' }); }).then( () => { return sander.unlink( dest ); @@ -960,7 +951,7 @@ describe( 'rollup', function () { }); }); - describe.only( 'rollup.watch', () => { + describe( 'rollup.watch', () => { beforeEach( () => { process.chdir(cwd); return sander.rimraf( 'test/_tmp' ); @@ -1025,14 +1016,18 @@ describe( 'rollup', function () { }); return sequence( watcher, [ - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.equal( run( './_tmp/output/bundle.js' ), 42 ); sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); }, - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.equal( run( './_tmp/output/bundle.js' ), 43 ); watcher.close(); @@ -1051,19 +1046,25 @@ describe( 'rollup', function () { }); return sequence( watcher, [ - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.equal( run( './_tmp/output/bundle.js' ), 42 ); sander.writeFileSync( 'test/_tmp/input/main.js', 'export nope;' ); }, - 'BUILD_START', + 'START', + 'BUNDLE_START', 'ERROR', + 'END', () => { sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); }, - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.equal( run( './_tmp/output/bundle.js' ), 43 ); watcher.close(); @@ -1082,21 +1083,26 @@ describe( 'rollup', function () { }); return sequence( watcher, [ - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.equal( run( './_tmp/output/bundle.js' ), 42 ); sander.unlinkSync( 'test/_tmp/input/main.js' ); sander.writeFileSync( 'test/_tmp/input/main.js', 'export nope;' ); }, - 'BUILD_START', + 'START', + 'BUNDLE_START', 'ERROR', () => { sander.unlinkSync( 'test/_tmp/input/main.js' ); sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); }, - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.equal( run( './_tmp/output/bundle.js' ), 43 ); watcher.close(); @@ -1115,20 +1121,25 @@ describe( 'rollup', function () { }); return sequence( watcher, [ - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.equal( run( './_tmp/output/bundle.js' ), 42 ); sander.writeFileSync( 'test/_tmp/input/main.js', `import '../output/bundle.js'` ); }, - 'BUILD_START', + 'START', + 'BUNDLE_START', 'ERROR', event => { assert.equal( event.error.message, 'Cannot import the generated bundle' ); sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); }, - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.equal( run( './_tmp/output/bundle.js' ), 43 ); watcher.close(); @@ -1150,14 +1161,18 @@ describe( 'rollup', function () { }); return sequence( watcher, [ - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.deepEqual( run( './_tmp/output/bundle.js' ), { foo: 'foo-1', bar: 'bar-1' }); sander.writeFileSync( 'test/_tmp/input/foo.js', `export default 'foo-2';` ); }, - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.deepEqual( run( './_tmp/output/bundle.js' ), { foo: 'foo-2', bar: 'bar-1' }); sander.writeFileSync( 'test/_tmp/input/bar.js', `export default 'bar-2';` ); @@ -1183,14 +1198,18 @@ describe( 'rollup', function () { }); return sequence( watcher, [ - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.deepEqual( run( './_tmp/output/bundle.js' ), { foo: 'foo-1', bar: 'bar-1' }); sander.writeFileSync( 'test/_tmp/input/foo.js', `export default 'foo-2';` ); }, - 'BUILD_START', - 'BUILD_END', + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', () => { assert.deepEqual( run( './_tmp/output/bundle.js' ), { foo: 'foo-2', bar: 'bar-1' }); sander.writeFileSync( 'test/_tmp/input/bar.js', `export default 'bar-2';` ); From 2a231b6c5a3156e8ab00f4eb112e705960a20f44 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Aug 2017 20:23:17 -0400 Subject: [PATCH 7/8] handle renamed files in watch mode --- src/watch/fileWatchers.js | 2 +- src/watch/index.js | 42 ++++++++++++++++++++++++++------------- test/test.js | 3 +-- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/watch/fileWatchers.js b/src/watch/fileWatchers.js index 910bdaf7ffe..85b5b2c61ed 100644 --- a/src/watch/fileWatchers.js +++ b/src/watch/fileWatchers.js @@ -60,8 +60,8 @@ export default class FileWatcher { const handleWatchEvent = event => { if (event === 'rename' || event === 'unlink') { this.fsWatcher.close(); - dispose(); this.trigger(); + dispose(); } else { // this is necessary because we get duplicate events... const contents = fs.readFileSync(id, 'utf-8'); diff --git a/src/watch/index.js b/src/watch/index.js index 58975948bc6..10b07f9c96a 100644 --- a/src/watch/index.js +++ b/src/watch/index.js @@ -48,25 +48,27 @@ class Watcher extends EventEmitter { code: 'START' }); - mapSequence(this.tasks, task => { - return task.run().catch(error => { + mapSequence(this.tasks, task => task.run()) + .then(() => { + this.succeeded = true; + this.emit('event', { - code: this.succeeded ? 'ERROR' : 'FATAL', - error + code: 'END' }); - }); - }).then(() => { - this.running = false; - if (this.dirty) { - this._run(); - } else { + }) + .catch(error => { this.emit('event', { - code: 'END' + code: this.succeeded ? 'ERROR' : 'FATAL', + error }); + }) + .then(() => { + this.running = false; - this.succeeded = true; - } - }); + if (this.dirty) { + this._run(); + } + }); } } @@ -76,6 +78,7 @@ class Task { this.watcher = watcher; this.options = options; + this.dirty = true; this.closed = false; this.watched = new Set(); @@ -120,6 +123,7 @@ class Task { } run() { + if (!this.dirty) return; this.dirty = false; const options = Object.assign(this.options, { @@ -176,6 +180,16 @@ class Task { output: this.dests, duration: Date.now() - start }); + }) + .catch(error => { + if (this.cache) { + this.cache.modules.forEach(module => { + // this is necessary to ensure that any 'renamed' files + // continue to be watched following an error + addTask(module.id, this, this.chokidarOptions, this.chokidarOptionsHash); + }); + } + throw error; }); } } diff --git a/test/test.js b/test/test.js index 24f787a71ac..cddd9d540f4 100644 --- a/test/test.js +++ b/test/test.js @@ -975,7 +975,7 @@ describe( 'rollup', function () { else if ( typeof next === 'string' ) { watcher.once( 'event', event => { if ( event.code !== next ) { - reject( new Error( `Expected ${next} error, got ${event.code}` ) ); + reject( new Error( `Expected ${next} event, got ${event.code}` ) ); } else { go( event ); } @@ -1057,7 +1057,6 @@ describe( 'rollup', function () { 'START', 'BUNDLE_START', 'ERROR', - 'END', () => { sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); }, From 9e481d890d333210de3364379b0b1004ee2169f7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Aug 2017 21:17:47 -0400 Subject: [PATCH 8/8] fix intermittent test failures --- src/watch/fileWatchers.js | 10 ++++++---- src/watch/index.js | 24 ++++++++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/watch/fileWatchers.js b/src/watch/fileWatchers.js index 85b5b2c61ed..15f69e4cbb1 100644 --- a/src/watch/fileWatchers.js +++ b/src/watch/fileWatchers.js @@ -29,11 +29,13 @@ export function deleteTask(id, target, chokidarOptionsHash) { const group = watchers.get(chokidarOptionsHash); const watcher = group.get(id); - watcher.tasks.delete(target); + if (watcher) { + watcher.tasks.delete(target); - if (watcher.tasks.size === 0) { - watcher.close(); - group.delete(id); + if (watcher.tasks.size === 0) { + watcher.close(); + group.delete(id); + } } } diff --git a/src/watch/index.js b/src/watch/index.js index 10b07f9c96a..4706d836d84 100644 --- a/src/watch/index.js +++ b/src/watch/index.js @@ -147,14 +147,8 @@ class Task { const watched = new Set(); bundle.modules.forEach(module => { - if (!this.filter(module.id)) return; - - if (~this.dests.indexOf(module.id)) { - throw new Error('Cannot import the generated bundle'); - } - watched.add(module.id); - addTask(module.id, this, this.chokidarOptions, this.chokidarOptionsHash); + this.watchFile(module.id); }); this.watched.forEach(id => { @@ -182,16 +176,30 @@ class Task { }); }) .catch(error => { + if (this.closed) return; + if (this.cache) { this.cache.modules.forEach(module => { // this is necessary to ensure that any 'renamed' files // continue to be watched following an error - addTask(module.id, this, this.chokidarOptions, this.chokidarOptionsHash); + this.watchFile(module.id); }); } throw error; }); } + + watchFile(id) { + if (!this.filter(id)) return; + + if (~this.dests.indexOf(id)) { + throw new Error('Cannot import the generated bundle'); + } + + // this is necessary to ensure that any 'renamed' files + // continue to be watched following an error + addTask(id, this, this.chokidarOptions, this.chokidarOptionsHash); + } } export default function watch(configs) {