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/.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/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/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 new file mode 100644 index 00000000000..1fbb69d9d5a --- /dev/null +++ b/bin/src/run/batchWarnings.js @@ -0,0 +1,275 @@ +import chalk from 'chalk'; +import { stderr } from '../logging.js'; +import relativeId from '../../../src/utils/relativeId.js'; + +export default function batchWarnings () { + let allWarnings = new Map(); + 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 ); + + count += 1; + }, + + flush: () => { + if ( count === 0 ) return; + + const codes = Array.from( allWarnings.keys() ) + .sort( ( a, b ) => { + if ( deferredHandlers[a] && deferredHandlers[b] ) { + return deferredHandlers[a].priority - deferredHandlers[b].priority; + } + + if ( deferredHandlers[a] ) return -1; + if ( deferredHandlers[b] ) return 1; + return allWarnings.get( b ).length - allWarnings.get( a ).length; + }); + + codes.forEach( code => { + const handler = deferredHandlers[ code ]; + const warnings = allWarnings.get( code ); + + if ( handler ) { + handler.fn( warnings ); + } else { + warnings.forEach( 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 ); + }); + } + }); + + allWarnings = new Map(); + } + }; +} + +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 deferredHandlers = { + UNUSED_EXTERNAL_IMPORT: { + priority: 1, + fn: warnings => { + title( 'Unused external imports' ); + warnings.forEach( warning => { + stderr( `${warning.names} imported from external module '${warning.source}' but never used` ); + }); + } + }, + + UNRESOLVED_IMPORT: { + priority: 1, + fn: warnings => { + title( '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 ); + stderr( `${chalk.bold( dependency )} (imported by ${importers.join( ', ' )})` ); + }); + } + }, + + MISSING_EXPORT: { + priority: 1, + fn: warnings => { + title( 'Missing exports' ); + info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module' ); + + warnings.forEach( warning => { + stderr( chalk.bold( warning.importer ) ); + stderr( `${warning.missing} is not exported by ${warning.exporter}` ); + stderr( chalk.grey( warning.frame ) ); + }); + } + }, + + THIS_IS_UNDEFINED: { + priority: 1, + fn: warnings => { + title( '`this` has been rewritten to `undefined`' ); + info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined' ); + 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)` ); + }); + } + }, + + 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}')`); + }); + } + }, + + SOURCEMAP_BROKEN: { + priority: 1, + fn: warnings => { + title( `Broken sourcemap` ); + info( 'https://github.com/rollup/rollup/wiki/Troubleshooting#sourcemap-is-likely-to-be-incorrect' ); + + 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]}')`; + + stderr( `Plugins that transform code${detail} should generate accompanying sourcemaps` ); + } + }, + + PLUGIN_WARNING: { + priority: 1, + fn: warnings => { + const nestedByPlugin = nest(warnings, 'plugin'); + + nestedByPlugin.forEach(({ key: plugin, items }) => { + const nestedByMessage = nest(items, 'message'); + + 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 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 new file mode 100644 index 00000000000..0ef94446991 --- /dev/null +++ b/bin/src/run/build.js @@ -0,0 +1,56 @@ +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, silent ) { + const useStdout = !options.targets && !options.dest; + const targets = options.targets ? options.targets : [{ dest: options.dest, format: options.format }]; + + const start = Date.now(); + 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 ( 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 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(); + if ( !silent ) stderr( chalk.green( `created ${chalk.bold( dests.join( ', ' ) )} in ${chalk.bold(ms( Date.now() - start))}` ) ); + }) + .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/run/index.js b/bin/src/run/index.js new file mode 100644 index 00000000000..5091e9fcef2 --- /dev/null +++ b/bin/src/run/index.js @@ -0,0 +1,145 @@ +import path from 'path'; +import chalk from 'chalk'; +import { realpathSync } from 'fs'; +import * as rollup from 'rollup'; +import relative from 'require-relative'; +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'; + +import { install as installSourcemapSupport } from 'source-map-support'; +installSourcemapSupport(); + +export default function runRollup ( command ) { + if ( command._.length > 1 ) { + handleError({ + code: 'ONE_AT_A_TIME', + message: 'rollup can only bundle one file at a time' + }); + } + + if ( command._.length === 1 ) { + if ( command.input ) { + handleError({ + code: 'DUPLICATE_IMPORT_OPTIONS', + message: 'use --input, or pass input path as argument' + }); + } + + command.input = command._[0]; + } + + if ( command.environment ) { + command.environment.split( ',' ).forEach( pair => { + const index = pair.indexOf( ':' ); + if ( ~index ) { + process.env[ pair.slice( 0, index ) ] = pair.slice( index + 1 ); + } else { + process.env[ pair ] = true; + } + }); + } + + let config = command.config === true ? 'rollup.config.js' : command.config; + + if ( config ) { + if ( config.slice( 0, 5 ) === 'node:' ) { + const pkgName = config.slice( 5 ); + try { + config = relative.resolve( `rollup-config-${pkgName}`, process.cwd() ); + } catch ( err ) { + try { + config = relative.resolve( pkgName, process.cwd() ); + } catch ( err ) { + if ( err.code === 'MODULE_NOT_FOUND' ) { + handleError({ + code: 'MISSING_EXTERNAL_CONFIG', + message: `Could not resolve config file ${config}` + }); + } + + throw err; + } + } + } else { + // find real path of config so it matches what Node provides to callbacks in require.extensions + config = realpathSync( config ); + } + + const warnings = batchWarnings(); + + rollup.rollup({ + entry: config, + external: id => { + return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5,id.length) === '.json'; + }, + onwarn: warnings.add + }) + .then( bundle => { + if ( !command.silent && warnings.count > 0 ) { + stderr( chalk.bold( `loaded ${relativeId( config )} with warnings` ) ); + warnings.flush(); + } + + return bundle.generate({ + format: 'cjs' + }); + }) + .then( ({ code }) => { + // temporarily override require + const defaultLoader = require.extensions[ '.js' ]; + require.extensions[ '.js' ] = ( m, filename ) => { + if ( filename === config ) { + m._compile( code, filename ); + } else { + defaultLoader( m, filename ); + } + }; + + const configs = require( config ); + 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 { + return execute( [{}], command ); + } +} + +function execute ( configs, command ) { + if ( command.watch ) { + process.env.ROLLUP_WATCH = 'true'; + watch( configs, command, command.silent ); + } else { + 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 new file mode 100644 index 00000000000..8b12514258f --- /dev/null +++ b/bin/src/run/mergeOptions.js @@ -0,0 +1,75 @@ +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; + } + + 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 ]; + } + }); + + 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 new file mode 100644 index 00000000000..751e98c83ab --- /dev/null +++ b/bin/src/run/watch.js @@ -0,0 +1,74 @@ +import * as rollup from 'rollup'; +import chalk from 'chalk'; +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, silent) { + process.stderr.write('\x1b[?1049h'); // alternate screen buffer + + 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; + } + + return merged; + }); + + const watcher = rollup.watch(configs); + + 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 'ERROR': + warnings.flush(); + handleError(event.error, true); + break; + + case 'START': + stderr(`\x1B[2J\x1B[0f${chalk.underline( 'rollup.watch' )}`); // clear, move to top-left + break; + + case 'BUNDLE_START': + if ( !silent ) stderr( chalk.cyan( `\n${chalk.bold( event.input )} → ${chalk.bold( event.output.map( relativeId ).join( ', ' ) )}...` ) ); + break; + + 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/bin/src/runRollup.js b/bin/src/runRollup.js deleted file mode 100644 index 4b6f6a4c134..00000000000 --- a/bin/src/runRollup.js +++ /dev/null @@ -1,287 +0,0 @@ -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 { install as installSourcemapSupport } from 'source-map-support'; -installSourcemapSupport(); - -export default function runRollup ( command ) { - if ( command._.length > 1 ) { - handleError({ - code: 'ONE_AT_A_TIME', - message: 'rollup can only bundle one file at a time' - }); - } - - if ( command._.length === 1 ) { - if ( command.input ) { - handleError({ - code: 'DUPLICATE_IMPORT_OPTIONS', - message: 'use --input, or pass input path as argument' - }); - } - - command.input = command._[0]; - } - - if ( command.environment ) { - command.environment.split( ',' ).forEach( pair => { - const index = pair.indexOf( ':' ); - if ( ~index ) { - process.env[ pair.slice( 0, index ) ] = pair.slice( index + 1 ); - } else { - process.env[ pair ] = true; - } - }); - } - - let config = command.config === true ? 'rollup.config.js' : command.config; - - if ( config ) { - if ( config.slice( 0, 5 ) === 'node:' ) { - const pkgName = config.slice( 5 ); - try { - config = relative.resolve( `rollup-config-${pkgName}`, process.cwd() ); - } catch ( err ) { - try { - config = relative.resolve( pkgName, process.cwd() ); - } catch ( err ) { - if ( err.code === 'MODULE_NOT_FOUND' ) { - handleError({ - code: 'MISSING_EXTERNAL_CONFIG', - message: `Could not resolve config file ${config}` - }); - } - - throw err; - } - } - } else { - // find real path of config so it matches what Node provides to callbacks in require.extensions - config = realpathSync( config ); - } - - rollup.rollup({ - entry: config, - external: id => { - return (id[0] !== '.' && !path.isAbsolute(id)) || id.slice(-5,id.length) === '.json'; - }, - onwarn: handleWarning - }) - .then( bundle => { - 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 ) => { - if ( filename === config ) { - m._compile( code, filename ); - } else { - defaultLoader( m, filename ); - } - }; - - 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 ); - }); - - require.extensions[ '.js' ] = defaultLoader; - }) - .catch( handleError ); - } else { - 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 = () => {}; - } - - 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. - Object.keys( equivalents ).forEach( cliOption => { - if ( command.hasOwnProperty( cliOption ) ) { - options[ equivalents[ cliOption ] ] = command[ cliOption ]; - } - }); - - 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 ); - } - } else { - bundle( 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/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..20a9568c5fc 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": { @@ -2417,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", @@ -3377,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", @@ -3443,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", @@ -3461,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", @@ -3811,6 +3864,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 +3982,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 +4002,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 +4143,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 +4230,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 +4250,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..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", @@ -67,15 +68,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' ], 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 216afad745a..8018505732e 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -216,11 +216,13 @@ export default class Bundle { if ( unused.length === 0 ) return; const names = unused.length === 1 ? - `'${unused[ 0 ]}' is` : - `${unused.slice( 0, -1 ).map( name => `'${name}'` ).join( ', ' )} and '${unused.pop()}' are`; + `'${unused[0]}' is` : + `${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 { @@ -389,6 +394,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' } ); @@ -447,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 cdd8526d575..4a1cf1fa0e8 100644 --- a/src/Module.js +++ b/src/Module.js @@ -182,28 +182,19 @@ 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 ); - } + 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 }; + }); } } @@ -461,6 +452,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/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index 3716cdc11f3..c55ff0a3702 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/ast/nodes/VariableDeclaration.js b/src/ast/nodes/VariableDeclaration.js index 3726705a43b..f34f12ff51f 100644 --- a/src/ast/nodes/VariableDeclaration.js +++ b/src/ast/nodes/VariableDeclaration.js @@ -88,7 +88,7 @@ export default class VariableDeclaration extends Node { } else { // always include a semi-colon (https://github.com/rollup/rollup/pull/1013), // unless it's a var declaration in a loop head - const needsSemicolon = !forStatement.test(this.parent.type); + const needsSemicolon = !forStatement.test( this.parent.type ) || this === this.parent.body; if (this.end > c) { code.overwrite(c, this.end, needsSemicolon ? ";" : ""); 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/iife.js b/src/finalisers/iife.js index 8b557d1ea02..34ed22e019d 100644 --- a/src/finalisers/iife.js +++ b/src/finalisers/iife.js @@ -72,7 +72,7 @@ export default function iife ( bundle, magicString, { exportMode, indentString, let wrapperOutro = `\n\n}(${dependencies}));`; - if (possibleVariableAssignment && exportMode === 'named') { + if (!extend && exportMode === 'named') { wrapperOutro = `\n\n${indentString}return exports;${wrapperOutro}`; } 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.js b/src/rollup.js index 732cb5eab52..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({ - 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..edc868d8fad --- /dev/null +++ b/src/rollup/index.js @@ -0,0 +1,188 @@ +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 === 'es6' ) { + throw new Error( 'The `es6` output format is deprecated – use `es` instead' ); + } + + if ( !options.format ) { + error({ // TODO make this an error + code: 'MISSING_FORMAT', + message: `You must supply an output format`, + 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/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/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..15f69e4cbb1 --- /dev/null +++ b/src/watch/fileWatchers.js @@ -0,0 +1,95 @@ + +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, 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) { + group.set(id, watcher); + } else { + return; + } + } + + group.get(id).tasks.add(task); +} + +export function deleteTask(id, target, chokidarOptionsHash) { + const group = watchers.get(chokidarOptionsHash); + + const watcher = group.get(id); + if (watcher) { + watcher.tasks.delete(target); + + if (watcher.tasks.size === 0) { + watcher.close(); + group.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(); + this.trigger(); + dispose(); + } 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..4706d836d84 --- /dev/null +++ b/src/watch/index.js @@ -0,0 +1,207 @@ +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; + +class Watcher extends EventEmitter { + constructor(configs) { + super(); + + this.dirty = true; + this.running = false; + this.tasks = ensureArray(configs).map(config => new Task(this, config)); + this.succeeded = false; + + process.nextTick(() => { + this._run(); + }); + } + + close() { + this.tasks.forEach(task => { + task.close(); + }); + } + + _makeDirty() { + if (this.dirty) return; + this.dirty = true; + + if (!this.running) { + setTimeout(() => { + this._run(); + }, DELAY); + } + } + + _run() { + this.running = true; + this.dirty = false; + + this.emit('event', { + code: 'START' + }); + + mapSequence(this.tasks, task => task.run()) + .then(() => { + this.succeeded = true; + + this.emit('event', { + code: 'END' + }); + }) + .catch(error => { + this.emit('event', { + code: this.succeeded ? 'ERROR' : 'FATAL', + error + }); + }) + .then(() => { + this.running = false; + + if (this.dirty) { + this._run(); + } + }); + } +} + +class Task { + constructor(watcher, options) { + this.cache = null; + this.watcher = watcher; + this.options = options; + + this.dirty = true; + this.closed = false; + this.watched = new Set(); + + 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, this.chokidarOptionsHash); + }); + } + + makeDirty() { + if (!this.dirty) { + this.dirty = true; + this.watcher._makeDirty(); + } + } + + run() { + if (!this.dirty) return; + this.dirty = false; + + const options = Object.assign(this.options, { + cache: this.cache + }); + + const start = Date.now(); + + this.watcher.emit('event', { + code: 'BUNDLE_START', + input: this.options.entry, + output: this.dests + }); + + return rollup(options) + .then(bundle => { + if (this.closed) return; + + this.cache = bundle; + + const watched = new Set(); + + bundle.modules.forEach(module => { + watched.add(module.id); + this.watchFile(module.id); + }); + + 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 + }); + }) + .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 + 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) { + return new Watcher(configs); +} \ No newline at end of file diff --git a/test/form/for-loop-body-var-declaration/_config.js b/test/form/for-loop-body-var-declaration/_config.js new file mode 100644 index 00000000000..9a0504cf967 --- /dev/null +++ b/test/form/for-loop-body-var-declaration/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'use a newline for line wraps removing a necessary semicolon (#1275)' +}; diff --git a/test/form/for-loop-body-var-declaration/_expected/amd.js b/test/form/for-loop-body-var-declaration/_expected/amd.js new file mode 100644 index 00000000000..65f8e046cf7 --- /dev/null +++ b/test/form/for-loop-body-var-declaration/_expected/amd.js @@ -0,0 +1,5 @@ +define(function () { 'use strict'; + + for(var x=1;x<2;x++)var d=x|0;console.log(d); + +}); diff --git a/test/form/for-loop-body-var-declaration/_expected/cjs.js b/test/form/for-loop-body-var-declaration/_expected/cjs.js new file mode 100644 index 00000000000..9398f8f1bbd --- /dev/null +++ b/test/form/for-loop-body-var-declaration/_expected/cjs.js @@ -0,0 +1,3 @@ +'use strict'; + +for(var x=1;x<2;x++)var d=x|0;console.log(d); diff --git a/test/form/for-loop-body-var-declaration/_expected/es.js b/test/form/for-loop-body-var-declaration/_expected/es.js new file mode 100644 index 00000000000..4f2606873e9 --- /dev/null +++ b/test/form/for-loop-body-var-declaration/_expected/es.js @@ -0,0 +1 @@ +for(var x=1;x<2;x++)var d=x|0;console.log(d); diff --git a/test/form/for-loop-body-var-declaration/_expected/iife.js b/test/form/for-loop-body-var-declaration/_expected/iife.js new file mode 100644 index 00000000000..1619ca1df29 --- /dev/null +++ b/test/form/for-loop-body-var-declaration/_expected/iife.js @@ -0,0 +1,6 @@ +(function () { + 'use strict'; + + for(var x=1;x<2;x++)var d=x|0;console.log(d); + +}()); diff --git a/test/form/for-loop-body-var-declaration/_expected/umd.js b/test/form/for-loop-body-var-declaration/_expected/umd.js new file mode 100644 index 00000000000..c50f8c4a11b --- /dev/null +++ b/test/form/for-loop-body-var-declaration/_expected/umd.js @@ -0,0 +1,9 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + for(var x=1;x<2;x++)var d=x|0;console.log(d); + +}))); diff --git a/test/form/for-loop-body-var-declaration/main.js b/test/form/for-loop-body-var-declaration/main.js new file mode 100644 index 00000000000..4f2606873e9 --- /dev/null +++ b/test/form/for-loop-body-var-declaration/main.js @@ -0,0 +1 @@ +for(var x=1;x<2;x++)var d=x|0;console.log(d); diff --git a/test/form/module-name-wat/_expected/iife.js b/test/form/module-name-wat/_expected/iife.js index 5d377d98182..ac64446b1af 100644 --- a/test/form/module-name-wat/_expected/iife.js +++ b/test/form/module-name-wat/_expected/iife.js @@ -8,4 +8,6 @@ this.foo['@scoped/npm-package'].bar['why-would-you-do-this'] = (function (export exports.foo = foo; + return exports; + }({})); diff --git a/test/form/namespaced-named-exports/_expected/iife.js b/test/form/namespaced-named-exports/_expected/iife.js index 4043b2692f9..5376c55e5d7 100644 --- a/test/form/namespaced-named-exports/_expected/iife.js +++ b/test/form/namespaced-named-exports/_expected/iife.js @@ -7,4 +7,6 @@ this.foo.bar.baz = (function (exports) { exports.answer = answer; + return exports; + }({})); diff --git a/test/form/umd-noconflict-namespaced/_expected/iife.js b/test/form/umd-noconflict-namespaced/_expected/iife.js index 40085ca93b5..201f2df1900 100644 --- a/test/form/umd-noconflict-namespaced/_expected/iife.js +++ b/test/form/umd-noconflict-namespaced/_expected/iife.js @@ -16,4 +16,6 @@ this.my.name.spaced.module = (function (exports) { exports.number = number; exports.setting = setting; + return exports; + }({})); 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..92a7eaac809 100644 --- a/test/function/namespace-missing-export/_config.js +++ b/test/function/namespace-missing-export/_config.js @@ -1,7 +1,13 @@ +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, loc: { diff --git a/test/function/unused-import/_config.js b/test/function/unused-import/_config.js index 0f99f594b94..8b7820d5b41 100644 --- a/test/function/unused-import/_config.js +++ b/test/function/unused-import/_config.js @@ -5,11 +5,15 @@ 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` }, { 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/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 diff --git a/test/test.js b/test/test.js index 1afe24fc399..cddd9d540f4 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 ); @@ -160,7 +168,7 @@ describe( 'rollup', function () { }); }); - it( 'warns on missing format option', () => { + it( 'throws on missing format option', () => { const warnings = []; return rollup.rollup({ @@ -168,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/ ); }); }); }); @@ -253,9 +256,7 @@ describe( 'rollup', function () { }); }); - it( 'warns on es6 format', () => { - let warned; - + it( 'throws on es6 format', () => { return rollup.rollup({ entry: 'x', plugins: [{ @@ -263,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/); }); }); }); @@ -856,7 +854,8 @@ describe( 'rollup', function () { ] }).then( bundle => { return bundle.write({ - dest + dest, + format: 'es' }); }).then( () => { return sander.unlink( dest ); @@ -951,4 +950,277 @@ describe( 'rollup', function () { }); }); }); + + describe( '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} event, 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, [ + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.equal( run( './_tmp/output/bundle.js' ), 42 ); + sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + '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, [ + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + 'END', + () => { + assert.equal( run( './_tmp/output/bundle.js' ), 42 ); + sander.writeFileSync( 'test/_tmp/input/main.js', 'export nope;' ); + }, + 'START', + 'BUNDLE_START', + 'ERROR', + () => { + sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + '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, [ + '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;' ); + }, + 'START', + 'BUNDLE_START', + 'ERROR', + () => { + sander.unlinkSync( 'test/_tmp/input/main.js' ); + sander.writeFileSync( 'test/_tmp/input/main.js', 'export default 43;' ); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + '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, [ + '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'` ); + }, + '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;' ); + }, + 'START', + 'BUNDLE_START', + 'BUNDLE_END', + '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, [ + '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';` ); + }, + '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';` ); + }, + () => { + 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, [ + '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';` ); + }, + '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';` ); + }, + () => { + 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