diff --git a/CHANGELOG.md b/CHANGELOG.md index 9408986..92fa262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # rollup changelog +## 0.40.1 + +* Allow missing space between `export default` and declaration ([#1218](https://github.com/rollup/rollup/pull/1218)) + +## 0.40.0 + +* [BREAKING] Better, more consistent error logging ([#1212](https://github.com/rollup/rollup/pull/1212)) +* Don't use colours and emojis for non-TTY stderr ([#1201](https://github.com/rollup/rollup/issues/1201)) + +## 0.39.2 + +* Prevent mutation of cached ASTs (fixes stack overflow with rollup-watch) ([#1205](https://github.com/rollup/rollup/pull/1205)) + +## 0.39.1 + +* Ignore `var` initialisers in dead branches ([#1198](https://github.com/rollup/rollup/issues/1198)) + +## 0.39.0 + +* [BREAKING] Warnings are objects, rather than strings ([#1194](https://github.com/rollup/rollup/issues/1194)) + +## 0.38.3 + +* More informative warning for implicit external dependencies ([#1051](https://github.com/rollup/rollup/issues/1051)) +* Warn when creating browser bundle with external dependencies on Node built-ins ([#1051](https://github.com/rollup/rollup/issues/1051)) +* Statically analyse LogicalExpression nodes, for better dead code removal ([#1061](https://github.com/rollup/rollup/issues/1061)) + ## 0.38.2 * Preserve `var` declarations in dead branches ([#997](https://github.com/rollup/rollup/issues/997)) diff --git a/bin/src/handleError.js b/bin/src/handleError.js deleted file mode 100644 index 80892fd..0000000 --- a/bin/src/handleError.js +++ /dev/null @@ -1,65 +0,0 @@ -import * as chalk from 'chalk'; - -function stderr ( msg ) { - console.error( msg ); // eslint-disable-line no-console -} - -const handlers = { - MISSING_CONFIG: () => { - stderr( chalk.red( 'Config file must export an options object. See https://github.com/rollup/rollup/wiki/Command-Line-Interface#using-a-config-file' ) ); - }, - - MISSING_EXTERNAL_CONFIG: err => { - stderr( chalk.red( `Could not resolve config file ${err.config}` ) ); - }, - - MISSING_INPUT_OPTION: () => { - stderr( chalk.red( 'You must specify an --input (-i) option' ) ); - }, - - MISSING_OUTPUT_OPTION: () => { - stderr( chalk.red( 'You must specify an --output (-o) option when creating a file with a sourcemap' ) ); - }, - - MISSING_NAME: () => { - stderr( chalk.red( 'You must supply a name for UMD exports (e.g. `--name myModule`)' ) ); - }, - - PARSE_ERROR: err => { - stderr( chalk.red( `Error parsing ${err.file}: ${err.message}` ) ); - }, - - ONE_AT_A_TIME: () => { - stderr( chalk.red( 'rollup can only bundle one file at a time' ) ); - }, - - DUPLICATE_IMPORT_OPTIONS: () => { - stderr( chalk.red( 'use --input, or pass input path as argument' ) ); - }, - - ROLLUP_WATCH_NOT_INSTALLED: () => { - stderr( chalk.red( 'rollup --watch depends on the rollup-watch package, which could not be found. You can install it globally (recommended) with ' ) + chalk.cyan( 'npm install -g rollup-watch' ) ); - }, - - WATCHER_MISSING_INPUT_OR_OUTPUT: () => { - stderr( chalk.red( 'must specify --input and --output when using rollup --watch' ) ); - } -}; - -export default function handleError ( err, recover ) { - const handler = handlers[ err && err.code ]; - - if ( handler ) { - handler( err ); - } else { - stderr( chalk.red( err.message || err ) ); - - if ( err.stack ) { - stderr( chalk.grey( err.stack ) ); - } - } - - stderr( `Type ${chalk.cyan( 'rollup --help' )} for help, or visit https://github.com/rollup/rollup/wiki` ); - - if ( !recover ) process.exit( 1 ); -} diff --git a/bin/src/logging.js b/bin/src/logging.js new file mode 100644 index 0000000..33b84af --- /dev/null +++ b/bin/src/logging.js @@ -0,0 +1,47 @@ +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 + +export function handleWarning ( warning ) { + stderr( `${warnSymbol}${chalk.bold( warning.message )}` ); + + if ( warning.url ) { + stderr( chalk.cyan( warning.url ) ); + } + + if ( warning.loc ) { + stderr( `${relativeId( warning.loc.file )} (${warning.loc.line}:${warning.loc.column})` ); + } + + if ( warning.frame ) { + stderr( chalk.dim( warning.frame ) ); + } + + stderr( '' ); +} + +export function handleError ( err, recover ) { + stderr( `${errorSymbol}${chalk.bold( err.message )}` ); + + if ( err.url ) { + stderr( chalk.cyan( err.url ) ); + } + + if ( err.loc ) { + stderr( `${relativeId( err.loc.file )} (${err.loc.line}:${err.loc.column})` ); + } + + if ( err.frame ) { + stderr( chalk.dim( err.frame ) ); + } + + stderr( '' ); + + if ( !recover ) process.exit( 1 ); +} diff --git a/bin/src/runRollup.js b/bin/src/runRollup.js index 63bbd0c..6b0130e 100644 --- a/bin/src/runRollup.js +++ b/bin/src/runRollup.js @@ -1,23 +1,27 @@ import { realpathSync } from 'fs'; import * as rollup from 'rollup'; import relative from 'require-relative'; -import handleError from './handleError'; +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(); -// stderr to stderr to keep `rollup main.js > bundle.js` from breaking -const stderr = console.error.bind( console ); // eslint-disable-line no-console - export default function runRollup ( command ) { if ( command._.length > 1 ) { - handleError({ code: 'ONE_AT_A_TIME' }); + 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' }); + handleError({ + code: 'DUPLICATE_IMPORT_OPTIONS', + message: 'use --input, or pass input path as argument' + }); } command.input = command._[0]; @@ -46,7 +50,10 @@ export default function runRollup ( command ) { config = relative.resolve( pkgName, process.cwd() ); } catch ( err ) { if ( err.code === 'MODULE_NOT_FOUND' ) { - handleError({ code: 'MISSING_EXTERNAL_CONFIG', config }); + handleError({ + code: 'MISSING_EXTERNAL_CONFIG', + message: `Could not resolve config file ${config}` + }); } throw err; @@ -59,38 +66,38 @@ export default function runRollup ( command ) { rollup.rollup({ entry: config, - onwarn: message => { - // TODO use warning codes instead of this hackery - if ( /treating it as an external dependency/.test( message ) ) return; - stderr( message ); + onwarn: warning => { + if ( warning.code === 'UNRESOLVED_IMPORT' ) return; + handleWarning( warning ); } - }).then( bundle => { - const { code } = bundle.generate({ - format: 'cjs' - }); + }) + .then( bundle => { + const { code } = bundle.generate({ + format: 'cjs' + }); - // temporarily override require - const defaultLoader = require.extensions[ '.js' ]; - require.extensions[ '.js' ] = ( m, filename ) => { - if ( filename === config ) { - m._compile( code, filename ); - } else { - defaultLoader( m, filename ); - } - }; + // temporarily override require + const defaultLoader = require.extensions[ '.js' ]; + require.extensions[ '.js' ] = ( m, filename ) => { + if ( filename === config ) { + m._compile( code, filename ); + } else { + defaultLoader( m, filename ); + } + }; - try { const options = require( config ); if ( Object.keys( options ).length === 0 ) { - handleError({ code: 'MISSING_CONFIG' }); + 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 ( err ) { - handleError( err ); - } - }) - .catch( stderr ); + }) + .catch( handleError ); } else { execute( {}, command ); } @@ -121,7 +128,7 @@ function execute ( options, command ) { const optionsExternal = options.external; if ( command.globals ) { - let globals = Object.create( null ); + const globals = Object.create( null ); command.globals.split( ',' ).forEach( str => { const names = str.split( ':' ); @@ -144,7 +151,18 @@ function execute ( options, command ) { external = ( optionsExternal || [] ).concat( commandExternal ); } - options.onwarn = options.onwarn || stderr; + 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; @@ -155,50 +173,52 @@ function execute ( options, command ) { } }); - try { - if ( command.watch ) { - if ( !options.entry || ( !options.dest && !options.targets ) ) { - handleError({ code: 'WATCHER_MISSING_INPUT_OR_OUTPUT' }); - } + 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 ); + try { + const watch = relative( 'rollup-watch', process.cwd() ); + const watcher = watch( rollup, options ); - watcher.on( 'event', event => { - switch ( event.code ) { - case 'STARTING': - stderr( 'checking rollup-watch version...' ); - break; + 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_START': + stderr( 'bundling...' ); + break; - case 'BUILD_END': - stderr( 'bundled in ' + event.duration + 'ms. Watching for changes...' ); - break; + case 'BUILD_END': + stderr( 'bundled in ' + event.duration + 'ms. Watching for changes...' ); + break; - case 'ERROR': - handleError( event.error, true ); - break; + case 'ERROR': + handleError( event.error, true ); + break; - default: - stderr( 'unknown event', event ); - } - }); - } catch ( err ) { - if ( err.code === 'MODULE_NOT_FOUND' ) { - err.code = 'ROLLUP_WATCH_NOT_INSTALLED'; + default: + stderr( 'unknown event', event ); } - - handleError( err ); + }); + } 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' + }); } - } else { - bundle( options ).catch( handleError ); + + handleError( err ); } - } catch ( err ) { - handleError( err ); + } else { + bundle( options ).catch( handleError ); } } @@ -215,34 +235,42 @@ function assign ( target, source ) { function bundle ( options ) { if ( !options.entry ) { - handleError({ code: 'MISSING_INPUT_OPTION' }); + handleError({ + code: 'MISSING_INPUT_OPTION', + message: 'You must specify an --input (-i) option' + }); } - return rollup.rollup( options ).then( bundle => { - if ( options.dest ) { - return bundle.write( options ); - } + return rollup.rollup( options ) + .then( bundle => { + if ( options.dest ) { + return bundle.write( options ); + } - if ( options.targets ) { - let result = null; + if ( options.targets ) { + let result = null; - options.targets.forEach( target => { - result = bundle.write( assign( clone( options ), target ) ); - }); + options.targets.forEach( target => { + result = bundle.write( assign( clone( options ), target ) ); + }); - return result; - } + return result; + } - if ( options.sourceMap && options.sourceMap !== 'inline' ) { - handleError({ code: 'MISSING_OUTPUT_OPTION' }); - } + 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' + }); + } - let { code, map } = bundle.generate( options ); + let { code, map } = bundle.generate( options ); - if ( options.sourceMap === 'inline' ) { - code += `\n//# ${SOURCEMAPPING_URL}=${map.toUrl()}\n`; - } + if ( options.sourceMap === 'inline' ) { + code += `\n//# ${SOURCEMAPPING_URL}=${map.toUrl()}\n`; + } - process.stdout.write( code ); - }); + process.stdout.write( code ); + }) + .catch( handleError ); } diff --git a/package.json b/package.json index 76ee18e..2ca30a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "0.38.2", + "version": "0.40.1", "description": "Next-generation ES6 module bundler", "main": "dist/rollup.js", "module": "dist/rollup.es.js", @@ -17,7 +17,7 @@ "posttest-coverage": "remap-istanbul -i coverage/coverage-final.json -o coverage/coverage-remapped.json -b dist && remap-istanbul -i coverage/coverage-final.json -o coverage/coverage-remapped.lcov -t lcovonly -b dist && remap-istanbul -i coverage/coverage-final.json -o coverage/coverage-remapped -t html -b dist", "ci": "npm run test-coverage && codecov < coverage/coverage-remapped.lcov", "build": "git rev-parse HEAD > .commithash && rollup -c", - "build:cli": "rollup -c rollup.config.cli.js", + "build:cli": "rollup -c rollup.config.cli.js && chmod a+x bin/rollup", "build:browser": "git rev-parse HEAD > .commithash && rollup -c rollup.config.browser.js", "watch": "rollup -c -w", "watch:browser": "rollup -c rollup.config.browser.js -w", @@ -49,23 +49,24 @@ "chalk": "^1.1.3", "codecov.io": "^0.1.6", "console-group": "^0.3.1", - "eslint": "^2.13.1", + "eslint": "^3.12.2", "eslint-plugin-import": "^2.2.0", "is-reference": "^1.0.0", "istanbul": "^0.4.3", + "locate-character": "^2.0.0", "magic-string": "^0.15.2", "minimist": "^1.2.0", "mocha": "^3.0.0", "remap-istanbul": "^0.6.4", "require-relative": "^0.8.7", - "rollup": "^0.34.0", - "rollup-plugin-buble": "^0.12.1", - "rollup-plugin-commonjs": "^3.0.0", + "rollup": "^0.39.0", + "rollup-plugin-buble": "^0.13.0", + "rollup-plugin-commonjs": "^7.0.0", "rollup-plugin-json": "^2.0.0", "rollup-plugin-node-resolve": "^2.0.0", "rollup-plugin-replace": "^1.1.0", "rollup-plugin-string": "^2.0.0", - "sander": "^0.5.1", + "sander": "^0.6.0", "source-map": "^0.5.6", "sourcemap-codec": "^1.3.0", "uglify-js": "^2.6.2" diff --git a/rollup.config.cli.js b/rollup.config.cli.js index 982f6b0..d8dae2a 100644 --- a/rollup.config.cli.js +++ b/rollup.config.cli.js @@ -14,8 +14,7 @@ export default { json(), buble(), commonjs({ - include: 'node_modules/**', - namedExports: { chalk: [ 'red', 'cyan', 'grey' ] } + include: 'node_modules/**' }), nodeResolve({ main: true diff --git a/src/Bundle.js b/src/Bundle.js index 1166cb2..0cfe895 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -17,6 +17,7 @@ import transformBundle from './utils/transformBundle.js'; import collapseSourcemaps from './utils/collapseSourcemaps.js'; import callIfFunction from './utils/callIfFunction.js'; import relativeId from './utils/relativeId.js'; +import error from './utils/error.js'; import { dirname, isRelative, isAbsolute, normalize, relative, resolve } from './utils/path.js'; import BundleScope from './ast/scopes/BundleScope.js'; @@ -106,7 +107,13 @@ export default class Bundle { // of the entry module's dependencies return this.resolveId( this.entry, undefined ) .then( id => { - if ( id == null ) throw new Error( `Could not resolve entry (${this.entry})` ); + if ( id == null ) { + error({ + code: 'UNRESOLVED_ENTRY', + message: `Could not resolve entry (${this.entry})` + }); + } + this.entryId = id; return this.fetchModule( id, undefined ); }) @@ -188,7 +195,10 @@ export default class Bundle { `'${unused[0]}' is` : `${unused.slice( 0, -1 ).map( name => `'${name}'` ).join( ', ' )} and '${unused.pop()}' are`; - this.onwarn( `${names} imported from external module '${module.id}' but never used` ); + this.warn({ + code: 'UNUSED_EXTERNAL_IMPORT', + message: `${names} imported from external module '${module.id}' but never used` + }); }); this.orderedModules = this.sort(); @@ -265,7 +275,11 @@ export default class Bundle { if ( typeof source === 'string' ) return source; if ( source && typeof source === 'object' && source.code ) return source; - throw new Error( `Error loading ${id}: load hook should return a string, a { code, map } object, or nothing/null` ); + // TODO report which plugin failed + error({ + code: 'BAD_LOADER', + message: `Error loading ${relativeId( id )}: plugin load hook should return a string, a { code, map } object, or nothing/null` + }); }) .then( source => { if ( typeof source === 'string' ) { @@ -309,7 +323,10 @@ export default class Bundle { keys( exportAllModule.exportsAll ).forEach( name => { if ( name in module.exportsAll ) { - this.onwarn( `Conflicting namespaces: ${module.id} re-exports '${name}' from both ${module.exportsAll[ name ]} (will be ignored) and ${exportAllModule.exportsAll[ name ]}.` ); + this.warn({ + code: 'NAMESPACE_CONFLICT', + message: `Conflicting namespaces: ${relativeId( module.id )} re-exports '${name}' from both ${relativeId( module.exportsAll[ name ] )} (will be ignored) and ${relativeId( exportAllModule.exportsAll[ name ] )}` + }); } module.exportsAll[ name ] = exportAllModule.exportsAll[ name ]; }); @@ -331,9 +348,18 @@ export default class Bundle { let isExternal = this.isExternal( externalId ); if ( !resolvedId && !isExternal ) { - if ( isRelative( source ) ) throw new Error( `Could not resolve '${source}' from ${module.id}` ); + if ( isRelative( source ) ) { + error({ + code: 'UNRESOLVED_IMPORT', + message: `Could not resolve '${source}' from ${relativeId( module.id )}` + }); + } - this.onwarn( `'${source}' is imported by ${relativeId( module.id )}, but could not be resolved – treating it as an external dependency. For help see https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency` ); + this.warn({ + code: 'UNRESOLVED_IMPORT', + 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' + }); isExternal = true; } @@ -357,7 +383,20 @@ export default class Bundle { }); } else { if ( resolvedId === module.id ) { - throw new Error( `A module cannot import itself (${resolvedId})` ); + // need to find the actual import declaration, so we can provide + // a useful error message. Bit hoop-jumpy but what can you do + const name = Object.keys( module.imports ) + .find( name => { + const declaration = module.imports[ name ]; + return declaration.source === source; + }); + + const declaration = module.imports[ name ].specifier.parent; + + module.error({ + code: 'CANNOT_IMPORT_SELF', + message: `A module cannot import itself` + }, declaration.start ); } module.resolvedIds[ source ] = resolvedId; @@ -380,12 +419,14 @@ export default class Bundle { render ( options = {} ) { if ( options.format === 'es6' ) { - this.onwarn( 'The es6 format is deprecated – use `es` instead' ); + this.warn({ + code: 'DEPRECATED_ES6', + message: 'The es6 format is deprecated – use `es` instead' + }); + options.format = 'es'; } - const format = options.format || 'es'; - // Determine export mode - 'default', 'named', 'none' const exportMode = getExportMode( this, options ); @@ -395,7 +436,7 @@ export default class Bundle { timeStart( 'render modules' ); this.orderedModules.forEach( module => { - const source = module.render( format === 'es', this.legacy ); + const source = module.render( options.format === 'es', this.legacy ); if ( source.toString().length ) { magicString.addSource( source ); @@ -404,7 +445,10 @@ export default class Bundle { }); if ( !magicString.toString().trim() && this.entryModule.getExports().length === 0 ) { - this.onwarn( 'Generated an empty bundle' ); + this.warn({ + code: 'EMPTY_BUNDLE', + message: 'Generated an empty bundle' + }); } timeEnd( 'render modules' ); @@ -429,8 +473,13 @@ export default class Bundle { const indentString = getIndentString( magicString, options ); - const finalise = finalisers[ format ]; - if ( !finalise ) throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` ); + const finalise = finalisers[ options.format ]; + if ( !finalise ) { + error({ + code: 'INVALID_OPTION', + message: `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` + }); + } timeStart( 'render format' ); @@ -470,7 +519,7 @@ export default class Bundle { if ( typeof map.mappings === 'string' ) { map.mappings = decode( map.mappings ); } - map = collapseSourcemaps( file, map, usedModules, bundleSourcemapChain, this.onwarn ); + map = collapseSourcemaps( this, file, map, usedModules, bundleSourcemapChain ); } else { map = magicString.generateMap({ file, includeContent: true }); } @@ -535,6 +584,7 @@ export default class Bundle { for ( i += 1; i < ordered.length; i += 1 ) { const b = ordered[i]; + // TODO reinstate this! it no longer works if ( stronglyDependsOn[ a.id ][ b.id ] ) { // somewhere, there is a module that imports b before a. Because // b imports a, a is placed before b. We need to find the module @@ -566,4 +616,16 @@ export default class Bundle { return ordered; } + + warn ( warning ) { + warning.toString = () => { + if ( warning.loc ) { + return `${relativeId( warning.loc.file )} (${warning.loc.line}:${warning.loc.column}) ${warning.message}`; + } + + return warning.message; + }; + + this.onwarn( warning ); + } } diff --git a/src/Module.js b/src/Module.js index 71db738..ce59dee 100644 --- a/src/Module.js +++ b/src/Module.js @@ -1,37 +1,41 @@ -import { timeStart, timeEnd } from './utils/flushTime.js'; import { parse } from 'acorn/src/index.js'; import MagicString from 'magic-string'; -import { assign, blank, deepClone, keys } from './utils/object.js'; +import { locate } from 'locate-character'; +import { timeStart, timeEnd } from './utils/flushTime.js'; +import { assign, blank, keys } from './utils/object.js'; import { basename, extname } from './utils/path.js'; -import getLocation from './utils/getLocation.js'; import makeLegalIdentifier from './utils/makeLegalIdentifier.js'; +import getCodeFrame from './utils/getCodeFrame.js'; import { SOURCEMAPPING_URL_RE } from './utils/sourceMappingURL.js'; import error from './utils/error.js'; import relativeId from './utils/relativeId.js'; import { SyntheticNamespaceDeclaration } from './Declaration.js'; import extractNames from './ast/utils/extractNames.js'; import enhance from './ast/enhance.js'; +import clone from './ast/clone.js'; import ModuleScope from './ast/scopes/ModuleScope.js'; -function tryParse ( code, comments, acornOptions, id ) { +function tryParse ( module, acornOptions ) { try { - return parse( code, assign({ + return parse( module.code, assign({ ecmaVersion: 8, sourceType: 'module', - onComment: ( block, text, start, end ) => comments.push({ block, text, start, end }), + onComment: ( block, text, start, end ) => module.comments.push({ block, text, start, end }), preserveParens: false }, acornOptions )); } catch ( err ) { - err.code = 'PARSE_ERROR'; - err.file = id; // see above - not necessarily true, but true enough - err.message += ` in ${id}`; - throw err; + module.error({ + code: 'PARSE_ERROR', + message: err.message.replace( / \(\d+:\d+\)$/, '' ) + }, err.pos ); } } export default class Module { constructor ({ id, code, originalCode, originalSourceMap, ast, sourceMapChain, resolvedIds, bundle }) { this.code = code; + this.id = id; + this.bundle = bundle; this.originalCode = originalCode; this.originalSourceMap = originalSourceMap; this.sourceMapChain = sourceMapChain; @@ -40,13 +44,18 @@ export default class Module { timeStart( 'ast' ); - this.ast = ast || tryParse( code, this.comments, bundle.acornOptions, id ); // TODO what happens to comments if AST is provided? - this.astClone = deepClone( this.ast ); + if ( ast ) { + // prevent mutating the provided AST, as it may be reused on + // subsequent incremental rebuilds + this.ast = clone( ast ); + this.astClone = ast; + } else { + this.ast = tryParse( this, bundle.acornOptions ); // TODO what happens to comments if AST is provided? + this.astClone = clone( this.ast ); + } timeEnd( 'ast' ); - this.bundle = bundle; - this.id = id; this.excludeFromSourcemap = /\0/.test( id ); this.context = bundle.getModuleContext( id ); @@ -112,7 +121,10 @@ export default class Module { const name = specifier.exported.name; if ( this.exports[ name ] || this.reexports[ name ] ) { - throw new Error( `A module cannot have multiple exports with the same name ('${name}')` ); + this.error({ + code: 'DUPLICATE_EXPORT', + message: `A module cannot have multiple exports with the same name ('${name}')` + }, specifier.start ); } this.reexports[ name ] = { @@ -132,8 +144,10 @@ export default class Module { const identifier = ( node.declaration.id && node.declaration.id.name ) || node.declaration.name; if ( this.exports.default ) { - // TODO indicate location - throw new Error( 'A module can only have one default export' ); + this.error({ + code: 'DUPLICATE_EXPORT', + message: `A module can only have one default export` + }, node.start ); } this.exports.default = { @@ -173,13 +187,21 @@ export default class Module { const exportedName = specifier.exported.name; if ( this.exports[ exportedName ] || this.reexports[ exportedName ] ) { - throw new Error( `A module cannot have multiple exports with the same name ('${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 { - this.bundle.onwarn( `Module ${this.id} has an empty export declaration` ); + // 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 ); } } } @@ -193,10 +215,10 @@ export default class Module { const localName = specifier.local.name; if ( this.imports[ localName ] ) { - const err = new Error( `Duplicated import '${localName}'` ); - err.file = this.id; - err.loc = getLocation( this.code, specifier.start ); - throw err; + this.error({ + code: 'DUPLICATE_IMPORT', + message: `Duplicated import '${localName}'` + }, specifier.start ); } const isDefault = specifier.type === 'ImportDefaultSpecifier'; @@ -268,6 +290,19 @@ export default class Module { // } } + error ( props, pos ) { + if ( pos !== undefined ) { + props.pos = pos; + + const { line, column } = locate( this.code, pos, { offsetLine: 1 }); // TODO trace sourcemaps + + props.loc = { file: this.id, line, column }; + props.frame = getCodeFrame( this.code, line, column ); + } + + error( props ); + } + findParent () { // TODO what does it mean if we're here? return null; @@ -358,11 +393,11 @@ export default class Module { const declaration = otherModule.traceExport( importDeclaration.name ); if ( !declaration ) { - error({ - message: `'${importDeclaration.name}' is not exported by ${relativeId( otherModule.id )} (imported by ${relativeId( this.id )}). For help fixing this error see https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module`, - file: this.id, - loc: getLocation( this.code, importDeclaration.specifier.start ) - }); + this.error({ + code: 'MISSING_EXPORT', + message: `'${importDeclaration.name}' is not exported by ${relativeId( otherModule.id )}`, + url: `https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module` + }, importDeclaration.specifier.start ); } return declaration; @@ -378,11 +413,11 @@ export default class Module { const declaration = reexportDeclaration.module.traceExport( reexportDeclaration.localName ); if ( !declaration ) { - error({ - message: `'${reexportDeclaration.localName}' is not exported by '${reexportDeclaration.module.id}' (imported by '${this.id}')`, - file: this.id, - loc: getLocation( this.code, reexportDeclaration.start ) - }); + this.error({ + code: 'MISSING_EXPORT', + message: `'${reexportDeclaration.localName}' is not exported by ${relativeId( reexportDeclaration.module.id )}`, + url: `https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module` + }, reexportDeclaration.start ); } return declaration; @@ -405,4 +440,17 @@ export default class Module { if ( declaration ) return declaration; } } + + warn ( warning, pos ) { + if ( pos !== undefined ) { + warning.pos = pos; + + const { line, column } = locate( this.code, pos, { offsetLine: 1 }); // TODO trace sourcemaps + + warning.loc = { file: this.id, line, column }; + warning.frame = getCodeFrame( this.code, line, column ); + } + + this.bundle.warn( warning ); + } } diff --git a/src/ast/Node.js b/src/ast/Node.js index 6a80ede..da36211 100644 --- a/src/ast/Node.js +++ b/src/ast/Node.js @@ -1,5 +1,5 @@ +import { locate } from 'locate-character'; import { UNKNOWN } from './values.js'; -import getLocation from '../utils/getLocation.js'; export default class Node { bind ( scope ) { @@ -74,7 +74,7 @@ export default class Node { locate () { // useful for debugging - const location = getLocation( this.module.code, this.start ); + const location = locate( this.module.code, this.start, { offsetLine: 1 }); location.file = this.module.id; location.toString = () => JSON.stringify( location ); diff --git a/src/ast/clone.js b/src/ast/clone.js new file mode 100644 index 0000000..604b372 --- /dev/null +++ b/src/ast/clone.js @@ -0,0 +1,17 @@ +export default function clone ( node ) { + if ( !node ) return node; + if ( typeof node !== 'object' ) return node; + + if ( Array.isArray( node ) ) { + const cloned = new Array( node.length ); + for ( let i = 0; i < node.length; i += 1 ) cloned[i] = clone( node[i] ); + return cloned; + } + + const cloned = {}; + for ( const key in node ) { + cloned[ key ] = clone( node[ key ] ); + } + + return cloned; +} diff --git a/src/ast/nodes/CallExpression.js b/src/ast/nodes/CallExpression.js index d275100..d743838 100644 --- a/src/ast/nodes/CallExpression.js +++ b/src/ast/nodes/CallExpression.js @@ -1,5 +1,3 @@ -import getLocation from '../../utils/getLocation.js'; -import error from '../../utils/error.js'; import Node from '../Node.js'; import isProgramLevel from '../utils/isProgramLevel.js'; import callHasEffects from './shared/callHasEffects.js'; @@ -10,16 +8,18 @@ export default class CallExpression extends Node { const declaration = scope.findDeclaration( this.callee.name ); if ( declaration.isNamespace ) { - error({ - message: `Cannot call a namespace ('${this.callee.name}')`, - file: this.module.id, - pos: this.start, - loc: getLocation( this.module.code, this.start ) - }); + this.module.error({ + code: 'CANNOT_CALL_NAMESPACE', + message: `Cannot call a namespace ('${this.callee.name}')` + }, this.start ); } if ( this.callee.name === 'eval' && declaration.isGlobal ) { - this.module.bundle.onwarn( `Use of \`eval\` (in ${this.module.id}) is strongly discouraged, as it poses security risks and may cause issues with minification. See https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval for more details` ); + this.module.warn({ + code: 'EVAL', + message: `Use of eval is strongly discouraged, as it poses security risks and may cause issues with minification`, + url: 'https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval' + }, this.start ); } } diff --git a/src/ast/nodes/ExportDefaultDeclaration.js b/src/ast/nodes/ExportDefaultDeclaration.js index d70bd10..2f505b1 100644 --- a/src/ast/nodes/ExportDefaultDeclaration.js +++ b/src/ast/nodes/ExportDefaultDeclaration.js @@ -1,6 +1,4 @@ import Node from '../Node.js'; -import getLocation from '../../utils/getLocation.js'; -import relativeId from '../../utils/relativeId.js'; const functionOrClassDeclaration = /^(?:Function|Class)Declaration/; @@ -55,7 +53,7 @@ export default class ExportDefaultDeclaration extends Node { let declaration_start; if ( this.declaration ) { const statementStr = code.original.slice( this.start, this.end ); - declaration_start = this.start + statementStr.match(/^\s*export\s+default\s+/)[0].length; + declaration_start = this.start + statementStr.match(/^\s*export\s+default\s*/)[0].length; } if ( this.shouldInclude || this.declaration.activated ) { @@ -74,8 +72,11 @@ export default class ExportDefaultDeclaration extends Node { const newlineSeparated = /\n/.test( code.original.slice( start, end ) ); if ( newlineSeparated ) { - const { line, column } = getLocation( this.module.code, this.declaration.start ); - this.module.bundle.onwarn( `${relativeId( this.module.id )} (${line}:${column}) Ambiguous default export (is a call expression, but looks like a function declaration). See https://github.com/rollup/rollup/wiki/Troubleshooting#ambiguous-default-export` ); + this.module.warn({ + code: 'AMBIGUOUS_DEFAULT_EXPORT', + message: `Ambiguous default export (is a call expression, but looks like a function declaration)`, + url: 'https://github.com/rollup/rollup/wiki/Troubleshooting#ambiguous-default-export' + }, this.declaration.start ); } } } diff --git a/src/ast/nodes/IfStatement.js b/src/ast/nodes/IfStatement.js index 87c8792..a16d3ca 100644 --- a/src/ast/nodes/IfStatement.js +++ b/src/ast/nodes/IfStatement.js @@ -17,9 +17,10 @@ function handleVarDeclarations ( node, scope ) { function visit ( node ) { if ( node.type === 'VariableDeclaration' && node.kind === 'var' ) { - node.initialise( scope ); - node.declarations.forEach( declarator => { + declarator.init = null; + declarator.initialise( scope ); + extractNames( declarator.id ).forEach( name => { if ( !~hoistedVars.indexOf( name ) ) hoistedVars.push( name ); }); diff --git a/src/ast/nodes/LogicalExpression.js b/src/ast/nodes/LogicalExpression.js new file mode 100644 index 0000000..4f81d3b --- /dev/null +++ b/src/ast/nodes/LogicalExpression.js @@ -0,0 +1,19 @@ +import Node from '../Node.js'; +import { UNKNOWN } from '../values.js'; + +const operators = { + '&&': ( left, right ) => left && right, + '||': ( left, right ) => left || right +}; + +export default class LogicalExpression extends Node { + getValue () { + const leftValue = this.left.getValue(); + if ( leftValue === UNKNOWN ) return UNKNOWN; + + const rightValue = this.right.getValue(); + if ( rightValue === UNKNOWN ) return UNKNOWN; + + return operators[ this.operator ]( leftValue, rightValue ); + } +} diff --git a/src/ast/nodes/MemberExpression.js b/src/ast/nodes/MemberExpression.js index bcdcf26..d745d7d 100644 --- a/src/ast/nodes/MemberExpression.js +++ b/src/ast/nodes/MemberExpression.js @@ -1,5 +1,4 @@ import isReference from 'is-reference'; -import getLocation from '../../utils/getLocation.js'; import relativeId from '../../utils/relativeId.js'; import Node from '../Node.js'; import { UNKNOWN } from '../values.js'; @@ -34,8 +33,11 @@ export default class MemberExpression extends Node { declaration = declaration.module.traceExport( part.name ); if ( !declaration ) { - const { line, column } = getLocation( this.module.code, this.start ); - this.module.bundle.onwarn( `${relativeId( this.module.id )} (${line}:${column}) '${part.name}' is not exported by '${relativeId( exporterId )}'. See https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module` ); + this.module.warn({ + code: 'MISSING_EXPORT', + message: `'${part.name}' is not exported by '${relativeId( exporterId )}'`, + url: `https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module` + }, part.start ); this.replacement = 'undefined'; return; } diff --git a/src/ast/nodes/ThisExpression.js b/src/ast/nodes/ThisExpression.js index ae35173..2429c65 100644 --- a/src/ast/nodes/ThisExpression.js +++ b/src/ast/nodes/ThisExpression.js @@ -1,8 +1,4 @@ import Node from '../Node.js'; -import getLocation from '../../utils/getLocation.js'; -import relativeId from '../../utils/relativeId.js'; - -const warning = `The 'this' keyword is equivalent to 'undefined' at the top level of an ES module, and has been rewritten. See https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined for more information`; export default class ThisExpression extends Node { initialise ( scope ) { @@ -11,9 +7,11 @@ export default class ThisExpression extends Node { if ( lexicalBoundary.isModuleScope ) { this.alias = this.module.context; if ( this.alias === 'undefined' ) { - const { line, column } = getLocation( this.module.code, this.start ); - const detail = `${relativeId( this.module.id )} (${line}:${column + 1})`; // use one-based column number convention - this.module.bundle.onwarn( `${detail} ${warning}` ); + this.module.warn({ + code: 'THIS_IS_UNDEFINED', + message: `The 'this' keyword is equivalent to 'undefined' at the top level of an ES module, and has been rewritten`, + url: `https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined` + }, this.start ); } } } diff --git a/src/ast/nodes/index.js b/src/ast/nodes/index.js index 1c3ba4c..2c59ee4 100644 --- a/src/ast/nodes/index.js +++ b/src/ast/nodes/index.js @@ -21,6 +21,7 @@ import Identifier from './Identifier.js'; import IfStatement from './IfStatement.js'; import ImportDeclaration from './ImportDeclaration.js'; import Literal from './Literal.js'; +import LogicalExpression from './LogicalExpression.js'; import MemberExpression from './MemberExpression.js'; import NewExpression from './NewExpression.js'; import ObjectExpression from './ObjectExpression.js'; @@ -59,6 +60,7 @@ export default { IfStatement, ImportDeclaration, Literal, + LogicalExpression, MemberExpression, NewExpression, ObjectExpression, diff --git a/src/ast/nodes/shared/disallowIllegalReassignment.js b/src/ast/nodes/shared/disallowIllegalReassignment.js index a40969f..17d054f 100644 --- a/src/ast/nodes/shared/disallowIllegalReassignment.js +++ b/src/ast/nodes/shared/disallowIllegalReassignment.js @@ -1,28 +1,21 @@ -import getLocation from '../../../utils/getLocation.js'; -import error from '../../../utils/error.js'; - // TODO tidy this up a bit (e.g. they can both use node.module.imports) export default function disallowIllegalReassignment ( scope, node ) { if ( node.type === 'MemberExpression' && node.object.type === 'Identifier' ) { const declaration = scope.findDeclaration( node.object.name ); if ( declaration.isNamespace ) { - error({ - message: `Illegal reassignment to import '${node.object.name}'`, - file: node.module.id, - pos: node.start, - loc: getLocation( node.module.code, node.start ) - }); + node.module.error({ + code: 'ILLEGAL_NAMESPACE_REASSIGNMENT', + message: `Illegal reassignment to import '${node.object.name}'` + }, node.start ); } } else if ( node.type === 'Identifier' ) { if ( node.module.imports[ node.name ] && !scope.contains( node.name ) ) { - error({ - message: `Illegal reassignment to import '${node.name}'`, - file: node.module.id, - pos: node.start, - loc: getLocation( node.module.code, node.start ) - }); + node.module.error({ + code: 'ILLEGAL_REASSIGNMENT', + message: `Illegal reassignment to import '${node.name}'` + }, node.start ); } } } diff --git a/src/ast/scopes/ModuleScope.js b/src/ast/scopes/ModuleScope.js index 4c8804c..f0f5f7a 100644 --- a/src/ast/scopes/ModuleScope.js +++ b/src/ast/scopes/ModuleScope.js @@ -1,4 +1,5 @@ import { forOwn } from '../../utils/object.js'; +import relativeId from '../../utils/relativeId.js'; import Scope from './Scope.js'; export default class ModuleScope extends Scope { @@ -26,7 +27,10 @@ export default class ModuleScope extends Scope { if ( specifier.name !== '*' ) { const declaration = specifier.module.traceExport( specifier.name ); if ( !declaration ) { - this.module.bundle.onwarn( `Non-existent export '${specifier.name}' is imported from ${specifier.module.id} by ${this.module.id}` ); + this.module.warn({ + code: 'NON_EXISTENT_EXPORT', + message: `Non-existent export '${specifier.name}' is imported from ${relativeId( specifier.module.id )}` + }, specifier.specifier.start ); return; } diff --git a/src/finalisers/amd.js b/src/finalisers/amd.js index 70b404d..efe1a8e 100644 --- a/src/finalisers/amd.js +++ b/src/finalisers/amd.js @@ -2,8 +2,10 @@ import { getName, quotePath } from '../utils/map-helpers.js'; import getInteropBlock from './shared/getInteropBlock.js'; import getExportBlock from './shared/getExportBlock.js'; import esModuleExport from './shared/esModuleExport.js'; +import warnOnBuiltins from './shared/warnOnBuiltins.js'; export default function amd ( bundle, magicString, { exportMode, indentString, intro, outro }, options ) { + warnOnBuiltins( bundle ); const deps = bundle.externalModules.map( quotePath ); const args = bundle.externalModules.map( getName ); diff --git a/src/finalisers/iife.js b/src/finalisers/iife.js index 60105c1..aad4491 100644 --- a/src/finalisers/iife.js +++ b/src/finalisers/iife.js @@ -1,9 +1,11 @@ import { blank } from '../utils/object.js'; import { getName } from '../utils/map-helpers.js'; +import error from '../utils/error.js'; import getInteropBlock from './shared/getInteropBlock.js'; import getExportBlock from './shared/getExportBlock.js'; import getGlobalNameMaker from './shared/getGlobalNameMaker.js'; import propertyStringFor from './shared/propertyStringFor'; +import warnOnBuiltins from './shared/warnOnBuiltins.js'; // thisProp('foo.bar-baz.qux') === "this.foo['bar-baz'].qux" const thisProp = propertyStringFor('this'); @@ -24,17 +26,21 @@ function setupNamespace ( keypath ) { } export default function iife ( bundle, magicString, { exportMode, indentString, intro, outro }, options ) { - const globalNameMaker = getGlobalNameMaker( options.globals || blank(), bundle.onwarn ); + const globalNameMaker = getGlobalNameMaker( options.globals || blank(), bundle ); const name = options.moduleName; const isNamespaced = name && ~name.indexOf( '.' ); - const dependencies = bundle.externalModules.map( globalNameMaker ); + warnOnBuiltins( bundle ); + const dependencies = bundle.externalModules.map( globalNameMaker ); const args = bundle.externalModules.map( getName ); if ( exportMode !== 'none' && !name ) { - throw new Error( 'You must supply options.moduleName for IIFE bundles' ); + error({ + code: 'INVALID_OPTION', + message: `You must supply options.moduleName for IIFE bundles` + }); } if ( exportMode === 'named' ) { diff --git a/src/finalisers/shared/getGlobalNameMaker.js b/src/finalisers/shared/getGlobalNameMaker.js index 676b416..7a1d622 100644 --- a/src/finalisers/shared/getGlobalNameMaker.js +++ b/src/finalisers/shared/getGlobalNameMaker.js @@ -1,11 +1,15 @@ -export default function getGlobalNameMaker ( globals, onwarn ) { +export default function getGlobalNameMaker ( globals, bundle ) { const fn = typeof globals === 'function' ? globals : id => globals[ id ]; return function ( module ) { const name = fn( module.id ); if ( name ) return name; - onwarn( `No name was provided for external module '${module.id}' in options.globals – guessing '${module.name}'` ); + bundle.warn({ + code: 'MISSING_GLOBAL_NAME', + message: `No name was provided for external module '${module.id}' in options.globals – guessing '${module.name}'` + }); + return module.name; }; } diff --git a/src/finalisers/shared/warnOnBuiltins.js b/src/finalisers/shared/warnOnBuiltins.js new file mode 100644 index 0000000..c4d63f6 --- /dev/null +++ b/src/finalisers/shared/warnOnBuiltins.js @@ -0,0 +1,42 @@ +const builtins = { + process: true, + events: true, + stream: true, + util: true, + path: true, + buffer: true, + querystring: true, + url: true, + string_decoder: true, + punycode: true, + http: true, + https: true, + os: true, + assert: true, + constants: true, + timers: true, + console: true, + vm: true, + zlib: true, + tty: true, + domain: true +}; + +// Creating a browser bundle that depends on Node.js built-in modules ('util'). You might need to include https://www.npmjs.com/package/rollup-plugin-node-builtins + +export default function warnOnBuiltins ( bundle ) { + const externalBuiltins = bundle.externalModules + .filter( mod => mod.id in builtins ) + .map( mod => mod.id ); + + if ( !externalBuiltins.length ) return; + + const detail = externalBuiltins.length === 1 ? + `module ('${externalBuiltins[0]}')` : + `modules (${externalBuiltins.slice( 0, -1 ).map( name => `'${name}'` ).join( ', ' )} and '${externalBuiltins.pop()}')`; + + bundle.warn({ + code: 'MISSING_NODE_BUILTINS', + 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/finalisers/umd.js b/src/finalisers/umd.js index 279226d..b624307 100644 --- a/src/finalisers/umd.js +++ b/src/finalisers/umd.js @@ -1,10 +1,12 @@ import { blank } from '../utils/object.js'; import { getName, quotePath, req } from '../utils/map-helpers.js'; +import error from '../utils/error.js'; import getInteropBlock from './shared/getInteropBlock.js'; import getExportBlock from './shared/getExportBlock.js'; import getGlobalNameMaker from './shared/getGlobalNameMaker.js'; import esModuleExport from './shared/esModuleExport.js'; import propertyStringFor from './shared/propertyStringFor.js'; +import warnOnBuiltins from './shared/warnOnBuiltins.js'; // globalProp('foo.bar-baz') === "global.foo['bar-baz']" const globalProp = propertyStringFor('global'); @@ -27,10 +29,15 @@ const wrapperOutro = '\n\n})));'; export default function umd ( bundle, magicString, { exportMode, indentString, intro, outro }, options ) { if ( exportMode !== 'none' && !options.moduleName ) { - throw new Error( 'You must supply options.moduleName for UMD bundles' ); + error({ + code: 'INVALID_OPTION', + message: 'You must supply options.moduleName for UMD bundles' + }); } - const globalNameMaker = getGlobalNameMaker( options.globals || blank(), bundle.onwarn ); + warnOnBuiltins( bundle ); + + const globalNameMaker = getGlobalNameMaker( options.globals || blank(), bundle ); const amdDeps = bundle.externalModules.map( quotePath ); const cjsDeps = bundle.externalModules.map( req ); diff --git a/src/rollup.js b/src/rollup.js index dfb537d..a87fcf8 100644 --- a/src/rollup.js +++ b/src/rollup.js @@ -4,6 +4,7 @@ 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'; @@ -50,15 +51,15 @@ function checkOptions ( options ) { return 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' ); } - const error = validateKeys( keys(options), ALLOWED_KEYS ); - if ( error ) return error; + const err = validateKeys( keys(options), ALLOWED_KEYS ); + if ( err ) return err; return null; } export function rollup ( options ) { - const error = checkOptions ( options ); - if ( error ) return Promise.reject( error ); + const err = checkOptions ( options ); + if ( err ) return Promise.reject( err ); const bundle = new Bundle( options ); @@ -67,7 +68,17 @@ export function rollup ( options ) { return bundle.build().then( () => { timeEnd( '--BUILD--' ); - function generate ( options ) { + 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'; + } + timeStart( '--GENERATE--' ); const rendered = bundle.render( options ); @@ -95,7 +106,10 @@ export function rollup ( options ) { generate, write: options => { if ( !options || !options.dest ) { - throw new Error( 'You must supply options.dest to bundle.write' ); + error({ + code: 'MISSING_OPTION', + message: 'You must supply options.dest to bundle.write' + }); } const dest = options.dest; diff --git a/src/utils/collapseSourcemaps.js b/src/utils/collapseSourcemaps.js index 7a504ba..bd65cf4 100644 --- a/src/utils/collapseSourcemaps.js +++ b/src/utils/collapseSourcemaps.js @@ -1,4 +1,5 @@ import { encode } from 'sourcemap-codec'; +import error from './error.js'; import { dirname, relative, resolve } from './path.js'; class Source { @@ -51,7 +52,9 @@ class Link { } else if ( sourcesContent[ sourceIndex ] == null ) { sourcesContent[ sourceIndex ] = traced.source.content; } else if ( traced.source.content != null && sourcesContent[ sourceIndex ] !== traced.source.content ) { - throw new Error( `Multiple conflicting contents for sourcemap source ${source.filename}` ); + error({ + message: `Multiple conflicting contents for sourcemap source ${source.filename}` + }); } segment[1] = sourceIndex; @@ -98,7 +101,7 @@ class Link { } } -export default function collapseSourcemaps ( file, map, modules, bundleSourcemapChain, onwarn ) { +export default function collapseSourcemaps ( bundle, file, map, modules, bundleSourcemapChain ) { const moduleSources = modules.filter( module => !module.excludeFromSourcemap ).map( module => { let sourceMapChain = module.sourceMapChain; @@ -127,7 +130,11 @@ export default function collapseSourcemaps ( file, map, modules, bundleSourcemap sourceMapChain.forEach( map => { if ( map.missing ) { - onwarn( `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 https://github.com/rollup/rollup/wiki/Troubleshooting and the plugin documentation for more information` ); + bundle.warn({ + code: 'SOURCEMAP_BROKEN', + 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` + }); map = { names: [], diff --git a/src/utils/defaults.js b/src/utils/defaults.js index 8db3c0b..d2757dd 100644 --- a/src/utils/defaults.js +++ b/src/utils/defaults.js @@ -1,6 +1,7 @@ import { lstatSync, readdirSync, readFileSync, realpathSync } from './fs.js'; // eslint-disable-line import { basename, dirname, isAbsolute, resolve } from './path.js'; import { blank } from './object.js'; +import error from './error.js'; export function load ( id ) { return readFileSync( id, 'utf-8' ); @@ -27,7 +28,13 @@ function addJsExtensionIfNecessary ( file ) { } export function resolveId ( importee, importer ) { - if ( typeof process === 'undefined' ) throw new Error( `It looks like you're using Rollup in a non-Node.js environment. This means you must supply a plugin with custom resolveId and load functions. See https://github.com/rollup/rollup/wiki/Plugins for more information` ); + if ( typeof process === 'undefined' ) { + error({ + code: 'MISSING_PROCESS', + message: `It looks like you're using Rollup in a non-Node.js environment. This means you must supply a plugin with custom resolveId and load functions`, + url: 'https://github.com/rollup/rollup/wiki/Plugins' + }); + } // absolute paths are left untouched if ( isAbsolute( importee ) ) return addJsExtensionIfNecessary( resolve( importee ) ); @@ -45,9 +52,10 @@ export function resolveId ( importee, importer ) { export function makeOnwarn () { const warned = blank(); - return msg => { - if ( msg in warned ) return; - console.error( msg ); //eslint-disable-line no-console - warned[ msg ] = true; + return warning => { + const str = warning.toString(); + if ( str in warned ) return; + console.error( str ); //eslint-disable-line no-console + warned[ str ] = true; }; } diff --git a/src/utils/getCodeFrame.js b/src/utils/getCodeFrame.js new file mode 100644 index 0000000..d5aa7a3 --- /dev/null +++ b/src/utils/getCodeFrame.js @@ -0,0 +1,41 @@ +function spaces ( i ) { + let result = ''; + while ( i-- ) result += ' '; + return result; +} + + +function tabsToSpaces ( str ) { + return str.replace( /^\t+/, match => match.split( '\t' ).join( ' ' ) ); +} + +export default function getCodeFrame ( source, line, column ) { + let lines = source.split( '\n' ); + + const frameStart = Math.max( 0, line - 3 ); + let frameEnd = Math.min( line + 2, lines.length ); + + lines = lines.slice( frameStart, frameEnd ); + while ( !/\S/.test( lines[ lines.length - 1 ] ) ) { + lines.pop(); + frameEnd -= 1; + } + + const digits = String( frameEnd ).length; + + return lines + .map( ( str, i ) => { + const isErrorLine = frameStart + i + 1 === line; + + let lineNum = String( i + frameStart + 1 ); + while ( lineNum.length < digits ) lineNum = ` ${lineNum}`; + + if ( isErrorLine ) { + const indicator = spaces( digits + 2 + tabsToSpaces( str.slice( 0, column ) ).length ) + '^'; + return `${lineNum}: ${tabsToSpaces( str )}\n${indicator}`; + } + + return `${lineNum}: ${tabsToSpaces( str )}`; + }) + .join( '\n' ); +} diff --git a/src/utils/getExportMode.js b/src/utils/getExportMode.js index a3d00a0..d61b128 100644 --- a/src/utils/getExportMode.js +++ b/src/utils/getExportMode.js @@ -1,7 +1,11 @@ import { keys } from './object.js'; +import error from './error.js'; function badExports ( option, keys ) { - throw new Error( `'${option}' was specified for options.exports, but entry module has following exports: ${keys.join(', ')}` ); + error({ + code: 'INVALID_EXPORT_OPTION', + message: `'${option}' was specified for options.exports, but entry module has following exports: ${keys.join(', ')}` + }); } export default function getExportMode ( bundle, {exports: exportMode, moduleName, format} ) { @@ -24,14 +28,21 @@ export default function getExportMode ( bundle, {exports: exportMode, moduleName exportMode = 'default'; } else { if ( bundle.entryModule.exports.default && format !== 'es') { - bundle.onwarn( `Using named and default exports together. Consumers of your bundle will have to use ${moduleName || 'bundle'}['default'] to access the default export, which may not be what you want. Use \`exports: 'named'\` to disable this warning. See https://github.com/rollup/rollup/wiki/JavaScript-API#exports for more information` ); + bundle.warn({ + code: 'MIXED_EXPORTS', + message: `Using named and default exports together. Consumers of your bundle will have to use ${moduleName || 'bundle'}['default'] to access the default export, which may not be what you want. Use \`exports: 'named'\` to disable this warning`, + url: `https://github.com/rollup/rollup/wiki/JavaScript-API#exports` + }); } exportMode = 'named'; } } if ( !/(?:default|named|none)/.test( exportMode ) ) { - throw new Error( `options.exports must be 'default', 'named', 'none', 'auto', or left unspecified (defaults to 'auto')` ); + error({ + code: 'INVALID_EXPORT_OPTION', + message: `options.exports must be 'default', 'named', 'none', 'auto', or left unspecified (defaults to 'auto')` + }); } return exportMode; diff --git a/src/utils/getLocation.js b/src/utils/getLocation.js deleted file mode 100644 index 41ca492..0000000 --- a/src/utils/getLocation.js +++ /dev/null @@ -1,20 +0,0 @@ -export default function getLocation ( source, charIndex ) { - const lines = source.split( '\n' ); - const len = lines.length; - - let lineStart = 0; - let i; - - for ( i = 0; i < len; i += 1 ) { - const line = lines[i]; - const lineEnd = lineStart + line.length + 1; // +1 for newline - - if ( lineEnd > charIndex ) { - return { line: i + 1, column: charIndex - lineStart }; - } - - lineStart = lineEnd; - } - - throw new Error( 'Could not determine location of character' ); -} diff --git a/src/utils/object.js b/src/utils/object.js index 947c8e1..4234de3 100644 --- a/src/utils/object.js +++ b/src/utils/object.js @@ -17,24 +17,3 @@ export function assign ( target, ...sources ) { return target; } - -const isArray = Array.isArray; - -// used for cloning ASTs. Not for use with cyclical structures! -export function deepClone ( obj ) { - if ( !obj ) return obj; - if ( typeof obj !== 'object' ) return obj; - - if ( isArray( obj ) ) { - const clone = new Array( obj.length ); - for ( let i = 0; i < obj.length; i += 1 ) clone[i] = deepClone( obj[i] ); - return clone; - } - - const clone = {}; - for ( const key in obj ) { - clone[ key ] = deepClone( obj[ key ] ); - } - - return clone; -} diff --git a/src/utils/transform.js b/src/utils/transform.js index 2f0c167..7e90a74 100644 --- a/src/utils/transform.js +++ b/src/utils/transform.js @@ -1,4 +1,6 @@ import { decode } from 'sourcemap-codec'; +import error from './error.js'; +import relativeId from './relativeId.js'; export default function transform ( source, id, plugins ) { const sourceMapChain = []; @@ -11,6 +13,7 @@ export default function transform ( source, id, plugins ) { const originalCode = source.code; let ast = source.ast; + let errored = false; return plugins.reduce( ( promise, plugin ) => { return promise.then( previous => { @@ -41,15 +44,21 @@ export default function transform ( source, id, plugins ) { return result.code; }); }).catch( err => { - if ( !err.rollupTransform ) { - err.rollupTransform = true; - err.id = id; - err.plugin = plugin.name; - err.message = `Error transforming ${id}${plugin.name ? ` with '${plugin.name}' plugin` : ''}: ${err.message}`; - } + // TODO this all seems a bit hacky + if ( errored ) throw err; + errored = true; + + err.plugin = plugin.name; throw err; }); }, Promise.resolve( source.code ) ) - - .then( code => ({ code, originalCode, originalSourceMap, ast, sourceMapChain }) ); + .catch( err => { + error({ + code: 'BAD_TRANSFORMER', + message: `Error transforming ${relativeId( id )}${err.plugin ? ` with '${err.plugin}' plugin` : ''}: ${err.message}`, + plugin: err.plugin, + id + }); + }) + .then( code => ({ code, originalCode, originalSourceMap, ast, sourceMapChain }) ); } diff --git a/src/utils/transformBundle.js b/src/utils/transformBundle.js index 8569ad5..03eb966 100644 --- a/src/utils/transformBundle.js +++ b/src/utils/transformBundle.js @@ -1,4 +1,5 @@ import { decode } from 'sourcemap-codec'; +import error from './error.js'; export default function transformBundle ( code, plugins, sourceMapChain, options ) { return plugins.reduce( ( code, plugin ) => { @@ -9,9 +10,11 @@ export default function transformBundle ( code, plugins, sourceMapChain, options try { result = plugin.transformBundle( code, { format : options.format } ); } catch ( err ) { - err.plugin = plugin.name; - err.message = `Error transforming bundle${plugin.name ? ` with '${plugin.name}' plugin` : ''}: ${err.message}`; - throw err; + error({ + code: 'BAD_BUNDLE_TRANSFORMER', + message: `Error transforming bundle${plugin.name ? ` with '${plugin.name}' plugin` : ''}: ${err.message}`, + plugin: plugin.name + }); } if ( result == null ) return code; diff --git a/test/cli/sourcemap-newline/_config.js b/test/cli/sourcemap-newline/_config.js index 1427619..8433548 100644 --- a/test/cli/sourcemap-newline/_config.js +++ b/test/cli/sourcemap-newline/_config.js @@ -2,7 +2,7 @@ const assert = require( 'assert' ); module.exports = { description: 'adds a newline after the sourceMappingURL comment (#756)', - command: 'rollup -i main.js -m inline', + command: 'rollup -i main.js -f es -m inline', result: code => { assert.equal( code.slice( -1 ), '\n' ); } diff --git a/test/form/skips-dead-branches-i/_config.js b/test/form/skips-dead-branches-i/_config.js index 85e8fbe..95aa942 100644 --- a/test/form/skips-dead-branches-i/_config.js +++ b/test/form/skips-dead-branches-i/_config.js @@ -1,3 +1,3 @@ module.exports = { - description: 'skips a dead branch (h)' + description: 'skips a dead branch (i)' }; diff --git a/test/form/skips-dead-branches-j/_config.js b/test/form/skips-dead-branches-j/_config.js new file mode 100644 index 0000000..b9abe10 --- /dev/null +++ b/test/form/skips-dead-branches-j/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'skips a dead branch (j)' +}; diff --git a/test/form/skips-dead-branches-j/_expected/amd.js b/test/form/skips-dead-branches-j/_expected/amd.js new file mode 100644 index 0000000..9ee9589 --- /dev/null +++ b/test/form/skips-dead-branches-j/_expected/amd.js @@ -0,0 +1,7 @@ +define(function () { 'use strict'; + + { + console.log( 'true' ); + } + +}); diff --git a/test/form/skips-dead-branches-j/_expected/cjs.js b/test/form/skips-dead-branches-j/_expected/cjs.js new file mode 100644 index 0000000..f71d9f9 --- /dev/null +++ b/test/form/skips-dead-branches-j/_expected/cjs.js @@ -0,0 +1,5 @@ +'use strict'; + +{ + console.log( 'true' ); +} diff --git a/test/form/skips-dead-branches-j/_expected/es.js b/test/form/skips-dead-branches-j/_expected/es.js new file mode 100644 index 0000000..bbd290a --- /dev/null +++ b/test/form/skips-dead-branches-j/_expected/es.js @@ -0,0 +1,3 @@ +{ + console.log( 'true' ); +} diff --git a/test/form/skips-dead-branches-j/_expected/iife.js b/test/form/skips-dead-branches-j/_expected/iife.js new file mode 100644 index 0000000..c506520 --- /dev/null +++ b/test/form/skips-dead-branches-j/_expected/iife.js @@ -0,0 +1,8 @@ +(function () { + 'use strict'; + + { + console.log( 'true' ); + } + +}()); diff --git a/test/form/skips-dead-branches-j/_expected/umd.js b/test/form/skips-dead-branches-j/_expected/umd.js new file mode 100644 index 0000000..2f6e70a --- /dev/null +++ b/test/form/skips-dead-branches-j/_expected/umd.js @@ -0,0 +1,11 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory() : + typeof define === 'function' && define.amd ? define(factory) : + (factory()); +}(this, (function () { 'use strict'; + + { + console.log( 'true' ); + } + +}))); diff --git a/test/form/skips-dead-branches-j/main.js b/test/form/skips-dead-branches-j/main.js new file mode 100644 index 0000000..afc8c67 --- /dev/null +++ b/test/form/skips-dead-branches-j/main.js @@ -0,0 +1,5 @@ +if ( true && true ) { + console.log( 'true' ); +} else { + console.log( 'false' ); +} diff --git a/test/function/assign-namespace-to-var/_config.js b/test/function/assign-namespace-to-var/_config.js index 2eb7e9c..a8c94ea 100644 --- a/test/function/assign-namespace-to-var/_config.js +++ b/test/function/assign-namespace-to-var/_config.js @@ -2,9 +2,10 @@ const assert = require( 'assert' ); module.exports = { description: 'allows a namespace to be assigned to a variable', - warnings: warnings => { - assert.deepEqual( warnings, [ - 'Generated an empty bundle' - ]); - } + warnings: [ + { + code: 'EMPTY_BUNDLE', + message: 'Generated an empty bundle' + } + ] }; diff --git a/test/function/cannot-call-external-namespace/_config.js b/test/function/cannot-call-external-namespace/_config.js index 263c14a..a891b1f 100644 --- a/test/function/cannot-call-external-namespace/_config.js +++ b/test/function/cannot-call-external-namespace/_config.js @@ -3,10 +3,19 @@ var assert = require( 'assert' ); module.exports = { description: 'errors if code calls an external namespace', - error: function ( err ) { - assert.equal( err.message, 'Cannot call a namespace (\'foo\')' ); - assert.equal( err.file.replace( /\//g, path.sep ), path.resolve( __dirname, 'main.js' ) ); - assert.equal( err.pos, 28 ); - assert.deepEqual( err.loc, { line: 2, column: 0 }); + error: { + code: 'CANNOT_CALL_NAMESPACE', + message: `Cannot call a namespace ('foo')`, + pos: 28, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 2, + column: 0 + }, + frame: ` + 1: import * as foo from 'foo'; + 2: foo(); + ^ + ` } }; diff --git a/test/function/cannot-call-internal-namespace/_config.js b/test/function/cannot-call-internal-namespace/_config.js index ceb42c3..6067234 100644 --- a/test/function/cannot-call-internal-namespace/_config.js +++ b/test/function/cannot-call-internal-namespace/_config.js @@ -3,10 +3,19 @@ var assert = require( 'assert' ); module.exports = { description: 'errors if code calls an internal namespace', - error: function ( err ) { - assert.equal( err.message, 'Cannot call a namespace (\'foo\')' ); - assert.equal( err.file.replace( /\//g, path.sep ), path.resolve( __dirname, 'main.js' ) ); - assert.equal( err.pos, 33 ); - assert.deepEqual( err.loc, { line: 2, column: 0 }); + error: { + code: 'CANNOT_CALL_NAMESPACE', + message: `Cannot call a namespace ('foo')`, + pos: 33, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 2, + column: 0 + }, + frame: ` + 1: import * as foo from './foo.js'; + 2: foo(); + ^ + ` } }; diff --git a/test/function/cannot-import-self/_config.js b/test/function/cannot-import-self/_config.js index 2413d03..f2896e9 100644 --- a/test/function/cannot-import-self/_config.js +++ b/test/function/cannot-import-self/_config.js @@ -1,8 +1,20 @@ +var path = require( 'path' ); var assert = require( 'assert' ); module.exports = { description: 'prevents a module importing itself', - error: function ( err ) { - assert.ok( /A module cannot import itself/.test( err.message ) ); + error: { + code: 'CANNOT_IMPORT_SELF', + message: `A module cannot import itself`, + pos: 0, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 1, + column: 0 + }, + frame: ` + 1: import me from './main'; + ^ + ` } }; diff --git a/test/function/check-resolve-for-entry/_config.js b/test/function/check-resolve-for-entry/_config.js index a06632d..cd3da5f 100644 --- a/test/function/check-resolve-for-entry/_config.js +++ b/test/function/check-resolve-for-entry/_config.js @@ -5,7 +5,8 @@ module.exports = { options: { entry: '/not/a/path/that/actually/really/exists' }, - error: function ( err ) { - assert.ok( /Could not resolve entry/.test( err.message ) ); + error: { + code: 'UNRESOLVED_ENTRY', + message: 'Could not resolve entry (/not/a/path/that/actually/really/exists)' } }; diff --git a/test/function/custom-external-resolver-async/_config.js b/test/function/custom-external-resolver-async/_config.js index 50e54fa..834f757 100644 --- a/test/function/custom-external-resolver-async/_config.js +++ b/test/function/custom-external-resolver-async/_config.js @@ -1,6 +1,5 @@ var path = require( 'path' ); var assert = require( 'assert' ); -var Promise = require( 'sander' ).Promise; module.exports = { description: 'uses a custom external path resolver (asynchronous)', diff --git a/test/function/custom-path-resolver-async/_config.js b/test/function/custom-path-resolver-async/_config.js index 83a10c1..c471e02 100644 --- a/test/function/custom-path-resolver-async/_config.js +++ b/test/function/custom-path-resolver-async/_config.js @@ -6,7 +6,6 @@ module.exports = { options: { plugins: [{ resolveId: function ( importee, importer ) { - var Promise = require( 'sander' ).Promise; var resolved; if ( path.normalize(importee) === path.resolve( __dirname, 'main.js' ) ) return importee; @@ -21,11 +20,13 @@ module.exports = { } }] }, - warnings: function ( warnings ) { - assert.deepEqual( warnings, [ - `'path' is imported by main.js, but could not be resolved – treating it as an external dependency. For help see https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency` - ]); - }, + warnings: [ + { + code: 'UNRESOLVED_IMPORT', + 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` + } + ], exports: function ( exports ) { assert.strictEqual( exports.path, require( 'path' ) ); } diff --git a/test/function/custom-path-resolver-plural-b/_config.js b/test/function/custom-path-resolver-plural-b/_config.js index cb2da0c..7ca6fe6 100644 --- a/test/function/custom-path-resolver-plural-b/_config.js +++ b/test/function/custom-path-resolver-plural-b/_config.js @@ -5,21 +5,21 @@ module.exports = { options: { plugins: [ { - resolveId: function () { + resolveId () { throw new Error( 'nope' ); }, - load: function ( id ) { + load ( id ) { if ( id === 'main' ) return 'assert.ok( false );'; } }, { - resolveId: function ( importee, importer ) { + resolveId ( importee, importer ) { return 'main'; } } ] }, - error: function ( err ) { - assert.equal( err.message, 'nope' ); + error: { + message: 'nope' } }; diff --git a/test/function/custom-path-resolver-sync/_config.js b/test/function/custom-path-resolver-sync/_config.js index 074f671..f32cb9a 100644 --- a/test/function/custom-path-resolver-sync/_config.js +++ b/test/function/custom-path-resolver-sync/_config.js @@ -13,11 +13,13 @@ module.exports = { } }] }, - warnings: function ( warnings ) { - assert.deepEqual( warnings, [ - `'path' is imported by main.js, but could not be resolved – treating it as an external dependency. For help see https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency` - ]); - }, + warnings: [ + { + code: 'UNRESOLVED_IMPORT', + 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` + } + ], exports: function ( exports ) { assert.strictEqual( exports.path, require( 'path' ) ); } diff --git a/test/function/default-not-reexported/_config.js b/test/function/default-not-reexported/_config.js index 4280924..3bd95b8 100644 --- a/test/function/default-not-reexported/_config.js +++ b/test/function/default-not-reexported/_config.js @@ -1,8 +1,23 @@ +const path = require( 'path' ); const assert = require( 'assert' ); module.exports = { description: 'default export is not re-exported with export *', - error ( error ) { - assert.equal( error.message, `'default' is not exported by foo.js (imported by main.js). For help fixing this error see https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module` ); + error: { + code: 'MISSING_EXPORT', + message: `'default' is not exported by foo.js`, + pos: 7, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 1, + column: 7 + }, + frame: ` + 1: import def from './foo.js'; + ^ + 2: + 3: console.log( def ); + `, + url: `https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module` } }; 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 901bfae..aeb5386 100644 --- a/test/function/does-not-hang-on-missing-module/_config.js +++ b/test/function/does-not-hang-on-missing-module/_config.js @@ -2,11 +2,13 @@ var assert = require( 'assert' ); module.exports = { description: 'does not hang on missing module (#53)', - warnings: warnings => { - assert.deepEqual( warnings, [ - `'unlessYouCreatedThisFileForSomeReason' is imported by main.js, but could not be resolved – treating it as an external dependency. For help see https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency` - ]); - }, + warnings: [ + { + code: 'UNRESOLVED_IMPORT', + 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` + } + ], runtimeError: function ( error ) { assert.equal( "Cannot find module 'unlessYouCreatedThisFileForSomeReason'", error.message ); } diff --git a/test/function/double-default-export/_config.js b/test/function/double-default-export/_config.js index d5f5254..3a9f85f 100644 --- a/test/function/double-default-export/_config.js +++ b/test/function/double-default-export/_config.js @@ -3,7 +3,19 @@ const assert = require( 'assert' ); module.exports = { description: 'throws on double default exports', - error: err => { - assert.equal( err.message, `Duplicate export 'default' (2:7) in ${path.resolve(__dirname, 'foo.js')}` ); + error: { + code: 'PARSE_ERROR', + message: `Duplicate export 'default'`, + pos: 25, + loc: { + file: path.resolve( __dirname, 'foo.js' ), + line: 2, + column: 7 + }, + frame: ` + 1: export default 1; + 2: export default 2; + ^ + ` } }; diff --git a/test/function/double-named-export-from/_config.js b/test/function/double-named-export-from/_config.js deleted file mode 100644 index b3885d2..0000000 --- a/test/function/double-named-export-from/_config.js +++ /dev/null @@ -1,13 +0,0 @@ -const { resolve } = require('path'); -const assert = require( 'assert' ); - -const r = path => resolve( __dirname, path ); - -module.exports = { - description: 'throws on duplicate export * from', - warnings ( warnings ) { - assert.deepEqual( warnings, [ - `Conflicting namespaces: ${r('main.js')} re-exports 'foo' from both ${r('foo.js')} (will be ignored) and ${r('deep.js')}.` - ]); - } -}; diff --git a/test/function/double-named-export/_config.js b/test/function/double-named-export/_config.js index 169ce76..0982751 100644 --- a/test/function/double-named-export/_config.js +++ b/test/function/double-named-export/_config.js @@ -3,7 +3,20 @@ const assert = require( 'assert' ); module.exports = { description: 'throws on duplicate named exports', - error: err => { - assert.equal( err.message, `Duplicate export 'foo' (3:9) in ${path.resolve(__dirname, 'foo.js')}` ); + error: { + code: 'PARSE_ERROR', + message: `Duplicate export 'foo'`, + pos: 38, + loc: { + file: path.resolve( __dirname, 'foo.js' ), + line: 3, + column: 9 + }, + frame: ` + 1: var foo = 1; + 2: export { foo }; + 3: export { foo }; + ^ + ` } }; diff --git a/test/function/double-named-reexport/_config.js b/test/function/double-named-reexport/_config.js index 169ce76..1c7ff1c 100644 --- a/test/function/double-named-reexport/_config.js +++ b/test/function/double-named-reexport/_config.js @@ -3,7 +3,20 @@ const assert = require( 'assert' ); module.exports = { description: 'throws on duplicate named exports', - error: err => { - assert.equal( err.message, `Duplicate export 'foo' (3:9) in ${path.resolve(__dirname, 'foo.js')}` ); + error: { + code: 'PARSE_ERROR', + message: `Duplicate export 'foo'`, + pos: 38, + loc: { + file: path.resolve( __dirname, 'foo.js' ), + line: 3, + column: 9 + }, + frame: ` + 1: var foo = 1; + 2: export { foo }; + 3: export { foo } from './bar.js'; + ^ + ` } }; diff --git a/test/function/duplicate-import-fails/_config.js b/test/function/duplicate-import-fails/_config.js index 4083690..f47de60 100644 --- a/test/function/duplicate-import-fails/_config.js +++ b/test/function/duplicate-import-fails/_config.js @@ -3,10 +3,22 @@ var assert = require( 'assert' ); module.exports = { description: 'disallows duplicate imports', - error: function ( err ) { - assert.equal( path.normalize(err.file), path.resolve( __dirname, 'main.js' ) ); - assert.deepEqual( err.loc, { line: 2, column: 9 }); - assert.ok( /Duplicated import/.test( err.message ) ); + error: { + code: 'DUPLICATE_IMPORT', + message: `Duplicated import 'a'`, + pos: 36, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 2, + column: 9 + }, + frame: ` + 1: import { a } from './foo'; + 2: import { a } from './foo'; + ^ + 3: + 4: assert.equal(a, 1); + ` } }; diff --git a/test/function/duplicate-import-specifier-fails/_config.js b/test/function/duplicate-import-specifier-fails/_config.js index e3957b5..35c88ce 100644 --- a/test/function/duplicate-import-specifier-fails/_config.js +++ b/test/function/duplicate-import-specifier-fails/_config.js @@ -3,10 +3,20 @@ var assert = require( 'assert' ); module.exports = { description: 'disallows duplicate import specifiers', - error: function ( err ) { - assert.equal( path.normalize(err.file), path.resolve( __dirname, 'main.js' ) ); - assert.deepEqual( err.loc, { line: 1, column: 12 }); - assert.ok( /Duplicated import/.test( err.message ) ); + error: { + code: 'DUPLICATE_IMPORT', + message: `Duplicated import 'a'`, + pos: 12, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 1, + column: 12 + }, + frame: ` + 1: import { a, a } from './foo'; + ^ + 2: assert.equal(a, 1); + ` } }; diff --git a/test/function/empty-exports/_config.js b/test/function/empty-exports/_config.js index 8030197..e928a59 100644 --- a/test/function/empty-exports/_config.js +++ b/test/function/empty-exports/_config.js @@ -1,12 +1,23 @@ -const assert = require( 'assert' ); -const path = require( 'path' ); - module.exports = { description: 'warns on export {}, but does not fail', - warnings: warnings => { - assert.deepEqual( warnings, [ - `Module ${path.resolve( __dirname, 'main.js' )} has an empty export declaration`, - 'Generated an empty bundle' - ]); - } + 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/export-default-no-space/_config.js b/test/function/export-default-no-space/_config.js new file mode 100644 index 0000000..8a27d49 --- /dev/null +++ b/test/function/export-default-no-space/_config.js @@ -0,0 +1,8 @@ +const assert = require( 'assert' ); + +module.exports = { + description: 'handles default exports with no space before declaration', + exports: exports => { + assert.deepEqual( exports, {} ); + } +}; diff --git a/test/function/export-default-no-space/main.js b/test/function/export-default-no-space/main.js new file mode 100644 index 0000000..f9749a6 --- /dev/null +++ b/test/function/export-default-no-space/main.js @@ -0,0 +1 @@ +export default{}; diff --git a/test/function/export-not-at-top-level-fails/_config.js b/test/function/export-not-at-top-level-fails/_config.js index 2dadeab..7fc1e3f 100644 --- a/test/function/export-not-at-top-level-fails/_config.js +++ b/test/function/export-not-at-top-level-fails/_config.js @@ -3,9 +3,20 @@ var assert = require( 'assert' ); module.exports = { description: 'disallows non-top-level exports', - error: function ( err ) { - assert.equal( path.normalize(err.file), path.resolve( __dirname, 'main.js' ) ); - assert.deepEqual( err.loc, { line: 2, column: 2 }); - assert.ok( /may only appear at the top level/.test( err.message ) ); + error: { + code: 'PARSE_ERROR', + message: `'import' and 'export' may only appear at the top level`, + pos: 19, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 2, + column: 2 + }, + frame: ` + 1: function foo() { + 2: export { foo }; + ^ + 3: } + ` } }; diff --git a/test/function/export-type-mismatch-b/_config.js b/test/function/export-type-mismatch-b/_config.js index 25a7220..e501330 100644 --- a/test/function/export-type-mismatch-b/_config.js +++ b/test/function/export-type-mismatch-b/_config.js @@ -5,7 +5,8 @@ module.exports = { bundleOptions: { exports: 'blah' }, - generateError: function ( err ) { - assert.ok( /options\.exports must be 'default', 'named', 'none', 'auto', or left unspecified/.test( err.message ) ); + generateError: { + code: 'INVALID_EXPORT_OPTION', + message: `options.exports must be 'default', 'named', 'none', 'auto', or left unspecified (defaults to 'auto')` } }; diff --git a/test/function/export-type-mismatch-c/_config.js b/test/function/export-type-mismatch-c/_config.js index 8df0d58..a1eca46 100644 --- a/test/function/export-type-mismatch-c/_config.js +++ b/test/function/export-type-mismatch-c/_config.js @@ -5,7 +5,8 @@ module.exports = { bundleOptions: { exports: 'none' }, - generateError: function ( err ) { - assert.ok( /'none' was specified for options\.exports/.test( err.message ) ); + generateError: { + code: 'INVALID_EXPORT_OPTION', + message: `'none' was specified for options.exports, but entry module has following exports: default` } }; diff --git a/test/function/export-type-mismatch/_config.js b/test/function/export-type-mismatch/_config.js index 5b321d6..1ff9fd5 100644 --- a/test/function/export-type-mismatch/_config.js +++ b/test/function/export-type-mismatch/_config.js @@ -5,7 +5,8 @@ module.exports = { bundleOptions: { exports: 'default' }, - generateError: function ( err ) { - assert.ok( /'default' was specified for options\.exports/.test( err.message ) ); + generateError: { + code: 'INVALID_EXPORT_OPTION', + message: `'default' was specified for options.exports, but entry module has following exports: foo` } }; diff --git a/test/function/import-not-at-top-level-fails/_config.js b/test/function/import-not-at-top-level-fails/_config.js index 4f873aa..4a68cc6 100644 --- a/test/function/import-not-at-top-level-fails/_config.js +++ b/test/function/import-not-at-top-level-fails/_config.js @@ -3,9 +3,20 @@ var assert = require( 'assert' ); module.exports = { description: 'disallows non-top-level imports', - error: function ( err ) { - assert.equal( path.normalize(err.file), path.resolve( __dirname, 'main.js' ) ); - assert.deepEqual( err.loc, { line: 2, column: 2 }); - assert.ok( /may only appear at the top level/.test( err.message ) ); + error: { + code: 'PARSE_ERROR', + message: `'import' and 'export' may only appear at the top level`, + pos: 19, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 2, + column: 2 + }, + frame: ` + 1: function foo() { + 2: import foo from './foo.js'; + ^ + 3: } + ` } }; diff --git a/test/function/import-not-at-top-level-fails/main.js b/test/function/import-not-at-top-level-fails/main.js index 1fa68e1..c88024f 100644 --- a/test/function/import-not-at-top-level-fails/main.js +++ b/test/function/import-not-at-top-level-fails/main.js @@ -1,3 +1,3 @@ function foo() { - import foo from './foo'; + import foo from './foo.js'; } diff --git a/test/function/import-of-unexported-fails/_config.js b/test/function/import-of-unexported-fails/_config.js index 10d11a6..3581bd9 100644 --- a/test/function/import-of-unexported-fails/_config.js +++ b/test/function/import-of-unexported-fails/_config.js @@ -1,8 +1,23 @@ +var path = require( 'path' ); var assert = require( 'assert' ); module.exports = { description: 'marking an imported, but unexported, identifier should throw', - error: function ( err ) { - assert.equal( err.message, `'default' is not exported by empty.js (imported by main.js). For help fixing this error see https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module` ); + error: { + code: 'MISSING_EXPORT', + message: `'default' is not exported by empty.js`, + pos: 7, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 1, + column: 7 + }, + frame: ` + 1: import a from './empty.js'; + ^ + 2: + 3: a(); + `, + url: `https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module` } }; diff --git a/test/function/load-returns-string-or-null/_config.js b/test/function/load-returns-string-or-null/_config.js index ea80699..b49e3eb 100644 --- a/test/function/load-returns-string-or-null/_config.js +++ b/test/function/load-returns-string-or-null/_config.js @@ -4,12 +4,14 @@ module.exports = { description: 'throws error if load returns something wacky', options: { plugins: [{ + name: 'bad-plugin', load: function () { return 42; } }] }, - error: function ( err ) { - assert.ok( /load hook should return a string, a \{ code, map \} object, or nothing\/null/.test( err.message ) ); + error: { + code: 'BAD_LOADER', + message: `Error loading main.js: plugin load hook should return a string, a { code, map } object, or nothing/null` } }; diff --git a/test/function/module-tree/_config.js b/test/function/module-tree/_config.js index 9662844..601ee7d 100644 --- a/test/function/module-tree/_config.js +++ b/test/function/module-tree/_config.js @@ -34,9 +34,10 @@ module.exports = { } ]); }, - warnings: warnings => { - assert.deepEqual( warnings, [ - 'Generated an empty bundle' - ]); - } + warnings: [ + { + code: 'EMPTY_BUNDLE', + message: 'Generated an empty bundle' + } + ] }; diff --git a/test/function/namespace-missing-export/_config.js b/test/function/namespace-missing-export/_config.js index 6ecf3b9..485a1ba 100644 --- a/test/function/namespace-missing-export/_config.js +++ b/test/function/namespace-missing-export/_config.js @@ -1,9 +1,21 @@ -var assert = require( 'assert' ); - module.exports = { - options: { - onwarn: function ( msg ) { - assert.equal( msg, `main.js (3:21) 'foo' is not exported by 'empty.js'. See https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module` ); + warnings: [ + { + code: 'MISSING_EXPORT', + message: `'foo' is not exported by 'empty.js'`, + pos: 61, + loc: { + file: require( 'path' ).resolve( __dirname, 'main.js' ), + line: 3, + column: 25 + }, + frame: ` + 1: import * as mod from './empty.js'; + 2: + 3: assert.equal( typeof mod.foo, 'undefined' ); + ^ + `, + url: `https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module` } - } + ] }; diff --git a/test/function/namespace-reassign-import-fails/_config.js b/test/function/namespace-reassign-import-fails/_config.js index 2d606f4..97335b1 100644 --- a/test/function/namespace-reassign-import-fails/_config.js +++ b/test/function/namespace-reassign-import-fails/_config.js @@ -3,10 +3,21 @@ var assert = require( 'assert' ); module.exports = { description: 'disallows reassignments to namespace exports', - error: function ( err ) { - assert.equal( path.normalize(err.file), path.resolve( __dirname, 'main.js' ) ); - assert.deepEqual( err.loc, { line: 3, column: 0 }); - assert.ok( /Illegal reassignment/.test( err.message ) ); + error: { + code: 'ILLEGAL_NAMESPACE_REASSIGNMENT', + message: `Illegal reassignment to import 'exp'`, + pos: 31, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 3, + column: 0 + }, + frame: ` + 1: import * as exp from './foo'; + 2: + 3: exp.foo = 2; + ^ + ` } }; diff --git a/test/function/namespace-update-import-fails/_config.js b/test/function/namespace-update-import-fails/_config.js index 4cff5c0..33ea524 100644 --- a/test/function/namespace-update-import-fails/_config.js +++ b/test/function/namespace-update-import-fails/_config.js @@ -3,10 +3,21 @@ var assert = require( 'assert' ); module.exports = { description: 'disallows updates to namespace exports', - error: function ( err ) { - assert.equal( path.normalize(err.file), path.resolve( __dirname, 'main.js' ) ); - assert.deepEqual( err.loc, { line: 3, column: 0 }); - assert.ok( /Illegal reassignment/.test( err.message ) ); + error: { + code: 'ILLEGAL_NAMESPACE_REASSIGNMENT', + message: `Illegal reassignment to import 'exp'`, + pos: 31, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 3, + column: 0 + }, + frame: ` + 1: import * as exp from './foo'; + 2: + 3: exp['foo']++; + ^ + ` } }; diff --git a/test/function/no-relative-external/_config.js b/test/function/no-relative-external/_config.js index b5c37e0..b87c9a1 100644 --- a/test/function/no-relative-external/_config.js +++ b/test/function/no-relative-external/_config.js @@ -2,7 +2,8 @@ var assert = require( 'assert' ); module.exports = { description: 'missing relative imports are an error, not a warning', - error: function ( err ) { - assert.ok( /Could not resolve '\.\/missing\.js' from/.test( err.message ) ); + error: { + code: 'UNRESOLVED_IMPORT', + message: `Could not resolve './missing.js' from main.js` } }; diff --git a/test/function/paths-are-case-sensitive/_config.js b/test/function/paths-are-case-sensitive/_config.js index 3f81b80..5b62cf7 100644 --- a/test/function/paths-are-case-sensitive/_config.js +++ b/test/function/paths-are-case-sensitive/_config.js @@ -2,7 +2,8 @@ var assert = require( 'assert' ); module.exports = { description: 'insists on correct casing for imports', - error: function ( err ) { - assert.ok( /Could not resolve/.test( err.message ) ); + error: { + code: 'UNRESOLVED_IMPORT', + message: `Could not resolve './foo.js' from main.js` } }; diff --git a/test/function/reassign-import-fails/_config.js b/test/function/reassign-import-fails/_config.js index 22591fe..922c41b 100644 --- a/test/function/reassign-import-fails/_config.js +++ b/test/function/reassign-import-fails/_config.js @@ -3,10 +3,21 @@ var assert = require( 'assert' ); module.exports = { description: 'disallows assignments to imported bindings', - error: function ( err ) { - assert.ok( /Illegal reassignment/.test( err.message ) ); - assert.equal( path.normalize(err.file), path.resolve( __dirname, 'main.js' ) ); - assert.deepEqual( err.loc, { line: 8, column: 0 }); + error: { + code: 'ILLEGAL_REASSIGNMENT', + message: `Illegal reassignment to import 'x'`, + pos: 113, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 8, + column: 0 + }, + frame: ` + 6: }); + 7: + 8: x = 10; + ^ + ` } }; diff --git a/test/function/reassign-import-not-at-top-level-fails/_config.js b/test/function/reassign-import-not-at-top-level-fails/_config.js index 6e00786..43b3a2c 100644 --- a/test/function/reassign-import-not-at-top-level-fails/_config.js +++ b/test/function/reassign-import-not-at-top-level-fails/_config.js @@ -3,10 +3,22 @@ var assert = require( 'assert' ); module.exports = { description: 'disallows assignments to imported bindings not at the top level', - error: function ( err ) { - assert.equal( path.normalize(err.file), path.resolve( __dirname, 'main.js' ) ); - assert.deepEqual( err.loc, { line: 7, column: 2 }); - assert.ok( /Illegal reassignment/.test( err.message ) ); + error: { + code: 'ILLEGAL_REASSIGNMENT', + message: `Illegal reassignment to import 'x'`, + pos: 95, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 7, + column: 2 + }, + frame: ` + 5: } + 6: export function bar () { + 7: x = 1; + ^ + 8: } + ` } }; diff --git a/test/function/reexport-missing-error/_config.js b/test/function/reexport-missing-error/_config.js index 6918683..66bcadc 100644 --- a/test/function/reexport-missing-error/_config.js +++ b/test/function/reexport-missing-error/_config.js @@ -1,8 +1,21 @@ +var path = require( 'path' ); var assert = require( 'assert' ); module.exports = { description: 'reexporting a missing identifier should print an error', - error: function ( error ) { - assert.ok( /^'foo' is not exported/.test( error.message ) ); + error: { + code: 'MISSING_EXPORT', + message: `'foo' is not exported by empty.js`, + pos: 9, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 1, + column: 9 + }, + frame: ` + 1: export { foo as bar } from './empty.js'; + ^ + `, + url: 'https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module' } }; diff --git a/test/function/report-transform-error-file/_config.js b/test/function/report-transform-error-file/_config.js index c856974..56dc1bc 100644 --- a/test/function/report-transform-error-file/_config.js +++ b/test/function/report-transform-error-file/_config.js @@ -1,9 +1,11 @@ +var path = require( 'path' ); var assert = require( 'assert' ); module.exports = { description: 'reports which file caused a transform error', options: { plugins: [{ + name: 'bad-plugin', transform: function ( code, id ) { if ( /foo/.test( id ) ) { throw new Error( 'nope' ); @@ -11,7 +13,10 @@ module.exports = { } }] }, - error: function ( err ) { - assert.ok( ~err.message.indexOf( 'foo.js' ) ); + error: { + code: 'BAD_TRANSFORMER', + message: `Error transforming foo.js with 'bad-plugin' plugin: nope`, + plugin: 'bad-plugin', + id: path.resolve( __dirname, 'foo.js' ) } }; diff --git a/test/function/reports-syntax-error-locations/_config.js b/test/function/reports-syntax-error-locations/_config.js deleted file mode 100644 index 2cb1085..0000000 --- a/test/function/reports-syntax-error-locations/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -var assert = require( 'assert' ); - -module.exports = { - description: 'reports syntax error filename', - error: function ( err ) { - assert.ok( /in .+main\.js/.test( err.message ) ); - } -}; diff --git a/test/function/reports-syntax-error-locations/main.js b/test/function/reports-syntax-error-locations/main.js deleted file mode 100644 index d24584b..0000000 --- a/test/function/reports-syntax-error-locations/main.js +++ /dev/null @@ -1 +0,0 @@ -var 42 = answer; diff --git a/test/function/throws-not-found-module/_config.js b/test/function/throws-not-found-module/_config.js index 04963a3..8aee10d 100644 --- a/test/function/throws-not-found-module/_config.js +++ b/test/function/throws-not-found-module/_config.js @@ -3,7 +3,8 @@ var path = require( 'path' ); module.exports = { description: 'throws error if module is not found', - error: function ( err ) { - assert.equal( err.message, 'Could not resolve \'./mod\' from ' + path.resolve( __dirname, 'main.js' ) ); + error: { + code: 'UNRESOLVED_IMPORT', + message: `Could not resolve './mod' from main.js` } -}; \ No newline at end of file +}; diff --git a/test/function/throws-only-first-transform-bundle/_config.js b/test/function/throws-only-first-transform-bundle/_config.js index 9097f52..21804e7 100644 --- a/test/function/throws-only-first-transform-bundle/_config.js +++ b/test/function/throws-only-first-transform-bundle/_config.js @@ -7,19 +7,20 @@ module.exports = { { name: 'plugin1', transformBundle: function () { - throw Error('Something happend 1'); + throw Error( 'Something happened 1' ); } }, { name: 'plugin2', transformBundle: function () { - throw Error('Something happend 2'); + throw Error( 'Something happened 2' ); } } ] }, - generateError: function ( err ) { - assert.equal( err.plugin, 'plugin1' ); - assert.equal( err.message, 'Error transforming bundle with \'plugin1\' plugin: Something happend 1' ); + generateError: { + code: 'BAD_BUNDLE_TRANSFORMER', + plugin: 'plugin1', + message: `Error transforming bundle with 'plugin1' plugin: Something happened 1` } -}; \ No newline at end of file +}; diff --git a/test/function/throws-only-first-transform/_config.js b/test/function/throws-only-first-transform/_config.js index c97907e..4bfd127 100644 --- a/test/function/throws-only-first-transform/_config.js +++ b/test/function/throws-only-first-transform/_config.js @@ -7,23 +7,22 @@ module.exports = { plugins: [ { name: 'plugin1', - transform: function () { - throw Error('Something happend 1'); + transform () { + throw Error( 'Something happened 1' ); } }, { name: 'plugin2', - transform: function () { - throw Error('Something happend 2'); + transform () { + throw Error( 'Something happened 2' ); } } ] }, - error: function ( err ) { - var id = path.resolve( __dirname, 'main.js' ); - assert.equal( err.rollupTransform, true ); - assert.equal( err.id, id ); - assert.equal( err.plugin, 'plugin1' ); - assert.equal( err.message, 'Error transforming ' + id + ' with \'plugin1\' plugin: Something happend 1' ); + error: { + code: 'BAD_TRANSFORMER', + message: `Error transforming main.js with 'plugin1' plugin: Something happened 1`, + plugin: 'plugin1', + id: path.resolve( __dirname, 'main.js' ) } -}; \ No newline at end of file +}; diff --git a/test/function/unused-import/_config.js b/test/function/unused-import/_config.js index b9e9b85..0f99f59 100644 --- a/test/function/unused-import/_config.js +++ b/test/function/unused-import/_config.js @@ -2,11 +2,19 @@ const assert = require( 'assert' ); module.exports = { description: 'warns on unused imports ([#595])', - warnings: warnings => { - assert.deepEqual( warnings, [ - `'external' is imported by main.js, but could not be resolved – treating it as an external dependency. For help see https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency`, - `'unused', 'notused' and 'neverused' are imported from external module 'external' but never used`, - `Generated an empty bundle` - ]); - } + warnings: [ + { + code: 'UNRESOLVED_IMPORT', + 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', + message: `'unused', 'notused' and 'neverused' are imported from external module 'external' but never used` + }, + { + code: 'EMPTY_BUNDLE', + message: `Generated an empty bundle` + } + ] }; diff --git a/test/function/update-expression-of-import-fails/_config.js b/test/function/update-expression-of-import-fails/_config.js index 5f5c27c..5e813c5 100644 --- a/test/function/update-expression-of-import-fails/_config.js +++ b/test/function/update-expression-of-import-fails/_config.js @@ -3,10 +3,21 @@ var assert = require( 'assert' ); module.exports = { description: 'disallows updates to imported bindings', - error: function ( err ) { - assert.equal( path.normalize(err.file), path.resolve( __dirname, 'main.js' ) ); - assert.deepEqual( err.loc, { line: 3, column: 0 }); - assert.ok( /Illegal reassignment/.test( err.message ) ); + error: { + code: 'ILLEGAL_REASSIGNMENT', + message: `Illegal reassignment to import 'a'`, + pos: 28, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 3, + column: 0 + }, + frame: ` + 1: import { a } from './foo'; + 2: + 3: a++; + ^ + ` } }; diff --git a/test/function/vars-with-init-in-dead-branch/_config.js b/test/function/vars-with-init-in-dead-branch/_config.js new file mode 100644 index 0000000..c3a1c46 --- /dev/null +++ b/test/function/vars-with-init-in-dead-branch/_config.js @@ -0,0 +1,9 @@ +module.exports = { + description: 'handles vars with init in dead branch (#1198)', + warnings: [ + { + code: 'EMPTY_BUNDLE', + message: 'Generated an empty bundle' + } + ] +}; diff --git a/test/function/vars-with-init-in-dead-branch/main.js b/test/function/vars-with-init-in-dead-branch/main.js new file mode 100644 index 0000000..a2c1863 --- /dev/null +++ b/test/function/vars-with-init-in-dead-branch/main.js @@ -0,0 +1,4 @@ +if ( false ) { + var foo = []; + var bar = foo.concat( 'x' ); +} diff --git a/test/function/warn-on-ambiguous-function-export/_config.js b/test/function/warn-on-ambiguous-function-export/_config.js index 1b83422..baa08bc 100644 --- a/test/function/warn-on-ambiguous-function-export/_config.js +++ b/test/function/warn-on-ambiguous-function-export/_config.js @@ -2,9 +2,23 @@ const assert = require( 'assert' ); module.exports = { description: 'uses original name of default export function (#1011)', - warnings: warnings => { - assert.deepEqual( warnings, [ - 'foo.js (1:15) Ambiguous default export (is a call expression, but looks like a function declaration). See https://github.com/rollup/rollup/wiki/Troubleshooting#ambiguous-default-export' - ]); - } + warnings: [ + { + code: 'AMBIGUOUS_DEFAULT_EXPORT', + message: `Ambiguous default export (is a call expression, but looks like a function declaration)`, + pos: 15, + loc: { + file: require( 'path' ).resolve( __dirname, 'foo.js' ), + line: 1, + column: 15 + }, + frame: ` + 1: export default function foo ( a, b ) { + ^ + 2: assert.equal( a, b ); + 3: return 3; + `, + url: `https://github.com/rollup/rollup/wiki/Troubleshooting#ambiguous-default-export` + } + ] }; diff --git a/test/function/warn-on-auto-named-default-exports/_config.js b/test/function/warn-on-auto-named-default-exports/_config.js index f093781..44e493b 100644 --- a/test/function/warn-on-auto-named-default-exports/_config.js +++ b/test/function/warn-on-auto-named-default-exports/_config.js @@ -1,10 +1,10 @@ -var assert = require( 'assert' ); - module.exports = { description: 'warns if default and named exports are used in auto mode', - warnings: function ( warnings ) { - assert.deepEqual( warnings, [ - 'Using named and default exports together. 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. See https://github.com/rollup/rollup/wiki/JavaScript-API#exports for more information' - ]); - } + warnings: [ + { + code: 'MIXED_EXPORTS', + message: `Using named and default exports together. 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`, + url: `https://github.com/rollup/rollup/wiki/JavaScript-API#exports` + } + ] }; diff --git a/test/function/warn-on-empty-bundle/_config.js b/test/function/warn-on-empty-bundle/_config.js index 2e599ad..a6d0a23 100644 --- a/test/function/warn-on-empty-bundle/_config.js +++ b/test/function/warn-on-empty-bundle/_config.js @@ -1,10 +1,9 @@ -const assert = require( 'assert' ); - module.exports = { description: 'warns if empty bundle is generated (#444)', - warnings: warnings => { - assert.deepEqual( warnings, [ - 'Generated an empty bundle' - ]); - } + warnings: [ + { + code: 'EMPTY_BUNDLE', + message: 'Generated an empty bundle' + } + ] }; diff --git a/test/function/warn-on-eval/_config.js b/test/function/warn-on-eval/_config.js index d6ac89f..a959bce 100644 --- a/test/function/warn-on-eval/_config.js +++ b/test/function/warn-on-eval/_config.js @@ -1,16 +1,20 @@ -var assert = require( 'assert' ); - -var warned = false; - module.exports = { description: 'warns about use of eval', - options: { - onwarn: function ( message ) { - warned = true; - assert.ok( /Use of `eval` \(in .+?main\.js\) is strongly discouraged, as it poses security risks and may cause issues with minification\. See https:\/\/github.com\/rollup\/rollup\/wiki\/Troubleshooting#avoiding-eval for more details/.test( message ) ); + warnings: [ + { + code: 'EVAL', + message: `Use of eval is strongly discouraged, as it poses security risks and may cause issues with minification`, + pos: 13, + loc: { + column: 13, + file: require( 'path' ).resolve( __dirname, 'main.js' ), + line: 1 + }, + frame: ` + 1: var result = eval( '1 + 1' ); + ^ + `, + url: 'https://github.com/rollup/rollup/wiki/Troubleshooting#avoiding-eval' } - }, - exports: function () { - assert.ok( warned, 'did not warn' ); - } + ] }; diff --git a/test/function/warn-on-namespace-conflict/_config.js b/test/function/warn-on-namespace-conflict/_config.js new file mode 100644 index 0000000..2054d75 --- /dev/null +++ b/test/function/warn-on-namespace-conflict/_config.js @@ -0,0 +1,9 @@ +module.exports = { + description: 'warns on duplicate export * from', + warnings: [ + { + code: 'NAMESPACE_CONFLICT', + message: `Conflicting namespaces: main.js re-exports 'foo' from both foo.js (will be ignored) and deep.js` + } + ] +}; diff --git a/test/function/double-named-export-from/bar.js b/test/function/warn-on-namespace-conflict/bar.js similarity index 100% rename from test/function/double-named-export-from/bar.js rename to test/function/warn-on-namespace-conflict/bar.js diff --git a/test/function/double-named-export-from/deep.js b/test/function/warn-on-namespace-conflict/deep.js similarity index 100% rename from test/function/double-named-export-from/deep.js rename to test/function/warn-on-namespace-conflict/deep.js diff --git a/test/function/double-named-export-from/foo.js b/test/function/warn-on-namespace-conflict/foo.js similarity index 100% rename from test/function/double-named-export-from/foo.js rename to test/function/warn-on-namespace-conflict/foo.js diff --git a/test/function/double-named-export-from/main.js b/test/function/warn-on-namespace-conflict/main.js similarity index 100% rename from test/function/double-named-export-from/main.js rename to test/function/warn-on-namespace-conflict/main.js diff --git a/test/function/warn-on-top-level-this/_config.js b/test/function/warn-on-top-level-this/_config.js index 99c3f2c..4d9033e 100644 --- a/test/function/warn-on-top-level-this/_config.js +++ b/test/function/warn-on-top-level-this/_config.js @@ -2,11 +2,25 @@ const assert = require( 'assert' ); module.exports = { description: 'warns on top-level this (#770)', - warnings: warnings => { - assert.deepEqual( warnings, [ - `main.js (3:1) The 'this' keyword is equivalent to 'undefined' at the top level of an ES module, and has been rewritten. See https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined for more information` - ]); - }, + warnings: [ + { + code: 'THIS_IS_UNDEFINED', + message: `The 'this' keyword is equivalent to 'undefined' at the top level of an ES module, and has been rewritten`, + pos: 81, + loc: { + file: require( 'path' ).resolve( __dirname, 'main.js' ), + line: 3, + column: 0 + }, + frame: ` + 1: const someVariableJustToCheckOnCorrectLineNumber = true; // eslint-disable-line + 2: + 3: this.foo = 'bar'; + ^ + `, + url: `https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined` + } + ], runtimeError: err => { assert.equal( err.message, `Cannot set property 'foo' of undefined` ); } diff --git a/test/function/warn-on-unused-missing-imports/_config.js b/test/function/warn-on-unused-missing-imports/_config.js index 1fca512..08cc6ba 100644 --- a/test/function/warn-on-unused-missing-imports/_config.js +++ b/test/function/warn-on-unused-missing-imports/_config.js @@ -3,9 +3,22 @@ const assert = require( 'assert' ); module.exports = { description: 'warns on missing (but unused) imports', - warnings: warnings => { - assert.deepEqual( warnings, [ - `Non-existent export 'b' is imported from ${path.resolve(__dirname, 'foo.js')} by ${path.resolve(__dirname, 'main.js')}` - ]); - } + warnings: [ + { + code: 'NON_EXISTENT_EXPORT', + message: `Non-existent export 'b' is imported from foo.js`, + pos: 12, + loc: { + file: path.resolve( __dirname, 'main.js' ), + line: 1, + column: 12 + }, + frame: ` + 1: import { a, b } from './foo.js'; + ^ + 2: + 3: assert.equal( a, 42 ); + ` + } + ] }; diff --git a/test/sourcemaps/transform-without-sourcemap/_config.js b/test/sourcemaps/transform-without-sourcemap/_config.js index ad33354..0ce79c0 100644 --- a/test/sourcemaps/transform-without-sourcemap/_config.js +++ b/test/sourcemaps/transform-without-sourcemap/_config.js @@ -1,10 +1,5 @@ -const assert = require( 'assert' ); - -let warnings = []; - module.exports = { description: 'preserves sourcemap chains when transforming', - before: () => warnings = [], // reset options: { plugins: [ { @@ -13,14 +8,13 @@ module.exports = { return code; } } - ], - onwarn ( msg ) { - warnings.push( msg ); - } + ] }, - test: () => { - assert.deepEqual( warnings, [ - `Sourcemap is likely to be incorrect: a plugin ('fake plugin') was used to transform files, but didn't generate a sourcemap for the transformation. Consult https://github.com/rollup/rollup/wiki/Troubleshooting and the plugin documentation for more information` - ]); - } + warnings: [ + { + code: `SOURCEMAP_BROKEN`, + message: `Sourcemap is likely to be incorrect: a plugin ('fake 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/test/test.js b/test/test.js index 8b2b66f..250b25a 100644 --- a/test/test.js +++ b/test/test.js @@ -49,7 +49,7 @@ function loadConfig ( path ) { function loader ( modules ) { return { resolveId ( id ) { - return id; + return id in modules ? id : null; }, load ( id ) { @@ -58,6 +58,44 @@ function loader ( modules ) { }; } +function compareWarnings ( actual, expected ) { + assert.deepEqual( + actual.map( warning => { + const clone = Object.assign( {}, warning ); + delete clone.toString; + + if ( clone.frame ) { + clone.frame = clone.frame.replace( /\s+$/gm, '' ); + } + + return clone; + }), + expected.map( warning => { + if ( warning.frame ) { + warning.frame = warning.frame.slice( 1 ).replace( /^\t+/gm, '' ).replace( /\s+$/gm, '' ).trim(); + } + return warning; + }) + ); +} + +function compareError ( actual, expected ) { + delete actual.stack; + actual = Object.assign( {}, actual, { + message: actual.message + }); + + if ( actual.frame ) { + actual.frame = actual.frame.replace( /\s+$/gm, '' ); + } + + if ( expected.frame ) { + expected.frame = expected.frame.slice( 1 ).replace( /^\t+/gm, '' ).replace( /\s+$/gm, '' ).trim(); + } + + assert.deepEqual( actual, expected ); +} + describe( 'rollup', function () { this.timeout( 10000 ); @@ -112,6 +150,25 @@ describe( 'rollup', function () { assert.ok( code[ code.length - 1 ] === '\n' ); }); }); + + it( 'warns on missing format option', () => { + const warnings = []; + + return rollup.rollup({ + entry: 'x', + 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` + } + ]); + }); + }); }); describe( 'bundle.write()', () => { @@ -219,7 +276,7 @@ describe( 'rollup', function () { } } catch ( err ) { if ( config.generateError ) { - config.generateError( err ); + compareError( err, config.generateError ); } else { unintendedError = err; } @@ -270,7 +327,11 @@ describe( 'rollup', function () { } if ( config.warnings ) { - config.warnings( warnings ); + if ( Array.isArray( config.warnings ) ) { + compareWarnings( warnings, config.warnings ); + } else { + config.warnings( warnings ); + } } else if ( warnings.length ) { throw new Error( `Got unexpected warnings:\n${warnings.join('\n')}` ); } @@ -280,7 +341,7 @@ describe( 'rollup', function () { if ( unintendedError ) throw unintendedError; }, err => { if ( config.error ) { - config.error( err ); + compareError( err, config.error ); } else { throw err; } @@ -377,11 +438,18 @@ describe( 'rollup', function () { const entry = path.resolve( SOURCEMAPS, dir, 'main.js' ); const dest = path.resolve( SOURCEMAPS, dir, '_actual/bundle' ); - const options = extend( {}, config.options, { entry }); + let warnings; + + const options = extend( {}, config.options, { + entry, + onwarn: warning => warnings.push( warning ) + }); PROFILES.forEach( profile => { ( config.skip ? it.skip : config.solo ? it.only : it )( 'generates ' + profile.format, () => { process.chdir( SOURCEMAPS + '/' + dir ); + warnings = []; + return rollup.rollup( options ).then( bundle => { const options = extend( {}, { format: profile.format, @@ -391,9 +459,16 @@ describe( 'rollup', function () { bundle.write( options ); - if ( config.before ) config.before(); - const result = bundle.generate( options ); - config.test( result.code, result.map ); + if ( config.test ) { + const { code, map } = bundle.generate( options ); + config.test( code, map ); + } + + if ( config.warnings ) { + compareWarnings( warnings, config.warnings ); + } else if ( warnings.length ) { + throw new Error( `Unexpected warnings` ); + } }); }); }); @@ -626,6 +701,35 @@ describe( 'rollup', function () { assert.deepEqual( asts.foo, acorn.parse( modules.foo, { sourceType: 'module' }) ); }); }); + + it( 'recovers from errors', () => { + modules.entry = `import foo from 'foo'; import bar from 'bar'; export default foo + bar;`; + + return rollup.rollup({ + entry: 'entry', + plugins: [ plugin ] + }).then( cache => { + modules.foo = `var 42 = nope;`; + + return rollup.rollup({ + entry: 'entry', + plugins: [ plugin ], + cache + }).catch( err => { + return cache; + }); + }).then( cache => { + modules.foo = `export default 42;`; + + return rollup.rollup({ + entry: 'entry', + plugins: [ plugin ], + cache + }).then( bundle => { + assert.equal( executeBundle( bundle ), 63 ); + }); + }); + }); }); describe( 'hooks', () => { @@ -710,8 +814,6 @@ describe( 'rollup', function () { dest, format: 'cjs' }); - - }).then( () => { assert.deepEqual( result, [ { a: dest, format: 'cjs' }, @@ -722,4 +824,27 @@ describe( 'rollup', function () { }); }); }); + + describe( 'misc', () => { + it( 'warns if node builtins are unresolved in a non-CJS, non-ES bundle (#1051)', () => { + const warnings = []; + + return rollup.rollup({ + entry: 'entry', + plugins: [ + loader({ entry: `import { format } from 'util';\nexport default format( 'this is a %s', 'formatted string' );` }) + ], + onwarn: warning => warnings.push( warning ) + }).then( bundle => { + bundle.generate({ + format: 'iife', + moduleName: 'myBundle' + }); + + const relevantWarnings = warnings.filter( warning => warning.code === 'MISSING_NODE_BUILTINS' ); + assert.equal( relevantWarnings.length, 1 ); + assert.equal( relevantWarnings[0].message, `Creating a browser bundle that depends on Node.js built-in module ('util'). You might need to include https://www.npmjs.com/package/rollup-plugin-node-builtins` ); + }); + }); + }); });