Browse Source

add module.error helper for consistently reporting errors (#545)

gh-1187
Rich-Harris 8 years ago
parent
commit
be0f82c623
  1. 24
      src/Bundle.js
  2. 43
      src/Module.js
  3. 10
      src/ast/nodes/CallExpression.js
  4. 19
      test/function/cannot-call-external-namespace/_config.js
  5. 19
      test/function/cannot-call-internal-namespace/_config.js
  6. 16
      test/function/cannot-import-self/_config.js
  7. 5
      test/function/check-resolve-for-entry/_config.js
  8. 10
      test/function/custom-path-resolver-plural-b/_config.js
  9. 19
      test/function/default-not-reexported/_config.js
  10. 16
      test/function/double-default-export/_config.js
  11. 17
      test/function/double-named-export/_config.js
  12. 17
      test/function/double-named-reexport/_config.js
  13. 19
      test/function/export-not-at-top-level-fails/_config.js
  14. 19
      test/function/import-not-at-top-level-fails/_config.js
  15. 2
      test/function/import-not-at-top-level-fails/main.js
  16. 19
      test/function/import-of-unexported-fails/_config.js
  17. 8
      test/function/reports-syntax-error-locations/_config.js
  18. 1
      test/function/reports-syntax-error-locations/main.js
  19. 23
      test/test.js

24
src/Bundle.js

@ -17,6 +17,7 @@ import transformBundle from './utils/transformBundle.js';
import collapseSourcemaps from './utils/collapseSourcemaps.js'; import collapseSourcemaps from './utils/collapseSourcemaps.js';
import callIfFunction from './utils/callIfFunction.js'; import callIfFunction from './utils/callIfFunction.js';
import relativeId from './utils/relativeId.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 { dirname, isRelative, isAbsolute, normalize, relative, resolve } from './utils/path.js';
import BundleScope from './ast/scopes/BundleScope.js'; import BundleScope from './ast/scopes/BundleScope.js';
@ -106,7 +107,13 @@ export default class Bundle {
// of the entry module's dependencies // of the entry module's dependencies
return this.resolveId( this.entry, undefined ) return this.resolveId( this.entry, undefined )
.then( id => { .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; this.entryId = id;
return this.fetchModule( id, undefined ); return this.fetchModule( id, undefined );
}) })
@ -367,7 +374,20 @@ export default class Bundle {
}); });
} else { } else {
if ( resolvedId === module.id ) { 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; module.resolvedIds[ source ] = resolvedId;

43
src/Module.js

@ -15,25 +15,27 @@ import enhance from './ast/enhance.js';
import clone from './ast/clone.js'; import clone from './ast/clone.js';
import ModuleScope from './ast/scopes/ModuleScope.js'; import ModuleScope from './ast/scopes/ModuleScope.js';
function tryParse ( code, comments, acornOptions, id ) { function tryParse ( module, acornOptions ) {
try { try {
return parse( code, assign({ return parse( module.code, assign({
ecmaVersion: 8, ecmaVersion: 8,
sourceType: 'module', 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 preserveParens: false
}, acornOptions )); }, acornOptions ));
} catch ( err ) { } catch ( err ) {
err.code = 'PARSE_ERROR'; module.error({
err.file = id; // see above - not necessarily true, but true enough code: 'PARSE_ERROR',
err.message += ` in ${id}`; message: err.message.replace( / \(\d+:\d+\)$/, '' )
throw err; }, err.pos );
} }
} }
export default class Module { export default class Module {
constructor ({ id, code, originalCode, originalSourceMap, ast, sourceMapChain, resolvedIds, bundle }) { constructor ({ id, code, originalCode, originalSourceMap, ast, sourceMapChain, resolvedIds, bundle }) {
this.code = code; this.code = code;
this.id = id;
this.bundle = bundle;
this.originalCode = originalCode; this.originalCode = originalCode;
this.originalSourceMap = originalSourceMap; this.originalSourceMap = originalSourceMap;
this.sourceMapChain = sourceMapChain; this.sourceMapChain = sourceMapChain;
@ -48,14 +50,12 @@ export default class Module {
this.ast = clone( ast ); this.ast = clone( ast );
this.astClone = ast; this.astClone = ast;
} else { } else {
this.ast = tryParse( code, this.comments, bundle.acornOptions, id ); // TODO what happens to comments if AST is provided? this.ast = tryParse( this, bundle.acornOptions ); // TODO what happens to comments if AST is provided?
this.astClone = clone( this.ast ); this.astClone = clone( this.ast );
} }
timeEnd( 'ast' ); timeEnd( 'ast' );
this.bundle = bundle;
this.id = id;
this.excludeFromSourcemap = /\0/.test( id ); this.excludeFromSourcemap = /\0/.test( id );
this.context = bundle.getModuleContext( id ); this.context = bundle.getModuleContext( id );
@ -282,6 +282,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 () { findParent () {
// TODO what does it mean if we're here? // TODO what does it mean if we're here?
return null; return null;
@ -372,11 +385,11 @@ export default class Module {
const declaration = otherModule.traceExport( importDeclaration.name ); const declaration = otherModule.traceExport( importDeclaration.name );
if ( !declaration ) { if ( !declaration ) {
error({ this.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`, code: 'MISSING_EXPORT',
file: this.id, message: `'${importDeclaration.name}' is not exported by ${relativeId( otherModule.id )}`,
loc: locate( this.code, importDeclaration.specifier.start, { offsetLine: 1 }) url: `https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module`
}); }, importDeclaration.specifier.start );
} }
return declaration; return declaration;

10
src/ast/nodes/CallExpression.js

@ -10,12 +10,10 @@ export default class CallExpression extends Node {
const declaration = scope.findDeclaration( this.callee.name ); const declaration = scope.findDeclaration( this.callee.name );
if ( declaration.isNamespace ) { if ( declaration.isNamespace ) {
error({ this.module.error({
message: `Cannot call a namespace ('${this.callee.name}')`, code: 'CANNOT_CALL_NAMESPACE',
file: this.module.id, message: `Cannot call a namespace ('${this.callee.name}')`
pos: this.start, }, this.start );
loc: locate( this.module.code, this.start, { offsetLine: 1 })
});
} }
if ( this.callee.name === 'eval' && declaration.isGlobal ) { if ( this.callee.name === 'eval' && declaration.isGlobal ) {

19
test/function/cannot-call-external-namespace/_config.js

@ -3,10 +3,19 @@ var assert = require( 'assert' );
module.exports = { module.exports = {
description: 'errors if code calls an external namespace', description: 'errors if code calls an external namespace',
error: function ( err ) { error: {
assert.equal( err.message, 'Cannot call a namespace (\'foo\')' ); code: 'CANNOT_CALL_NAMESPACE',
assert.equal( err.file.replace( /\//g, path.sep ), path.resolve( __dirname, 'main.js' ) ); message: `Cannot call a namespace ('foo')`,
assert.equal( err.pos, 28 ); pos: 28,
assert.deepEqual( err.loc, { character: 28, line: 2, column: 0 }); loc: {
file: path.resolve( __dirname, 'main.js' ),
line: 2,
column: 0
},
frame: `
1: import * as foo from 'foo';
2: foo();
^
`
} }
}; };

19
test/function/cannot-call-internal-namespace/_config.js

@ -3,10 +3,19 @@ var assert = require( 'assert' );
module.exports = { module.exports = {
description: 'errors if code calls an internal namespace', description: 'errors if code calls an internal namespace',
error: function ( err ) { error: {
assert.equal( err.message, 'Cannot call a namespace (\'foo\')' ); code: 'CANNOT_CALL_NAMESPACE',
assert.equal( err.file.replace( /\//g, path.sep ), path.resolve( __dirname, 'main.js' ) ); message: `Cannot call a namespace ('foo')`,
assert.equal( err.pos, 33 ); pos: 33,
assert.deepEqual( err.loc, { character: 33, line: 2, column: 0 }); loc: {
file: path.resolve( __dirname, 'main.js' ),
line: 2,
column: 0
},
frame: `
1: import * as foo from './foo.js';
2: foo();
^
`
} }
}; };

16
test/function/cannot-import-self/_config.js

@ -1,8 +1,20 @@
var path = require( 'path' );
var assert = require( 'assert' ); var assert = require( 'assert' );
module.exports = { module.exports = {
description: 'prevents a module importing itself', description: 'prevents a module importing itself',
error: function ( err ) { error: {
assert.ok( /A module cannot import itself/.test( err.message ) ); 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';
^
`
} }
}; };

5
test/function/check-resolve-for-entry/_config.js

@ -5,7 +5,8 @@ module.exports = {
options: { options: {
entry: '/not/a/path/that/actually/really/exists' entry: '/not/a/path/that/actually/really/exists'
}, },
error: function ( err ) { error: {
assert.ok( /Could not resolve entry/.test( err.message ) ); code: 'UNRESOLVED_ENTRY',
message: 'Could not resolve entry (/not/a/path/that/actually/really/exists)'
} }
}; };

10
test/function/custom-path-resolver-plural-b/_config.js

@ -5,21 +5,21 @@ module.exports = {
options: { options: {
plugins: [ plugins: [
{ {
resolveId: function () { resolveId () {
throw new Error( 'nope' ); throw new Error( 'nope' );
}, },
load: function ( id ) { load ( id ) {
if ( id === 'main' ) return 'assert.ok( false );'; if ( id === 'main' ) return 'assert.ok( false );';
} }
}, },
{ {
resolveId: function ( importee, importer ) { resolveId ( importee, importer ) {
return 'main'; return 'main';
} }
} }
] ]
}, },
error: function ( err ) { error: {
assert.equal( err.message, 'nope' ); message: 'nope'
} }
}; };

19
test/function/default-not-reexported/_config.js

@ -1,8 +1,23 @@
const path = require( 'path' );
const assert = require( 'assert' ); const assert = require( 'assert' );
module.exports = { module.exports = {
description: 'default export is not re-exported with export *', description: 'default export is not re-exported with export *',
error ( 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` ); 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`
} }
}; };

16
test/function/double-default-export/_config.js

@ -3,7 +3,19 @@ const assert = require( 'assert' );
module.exports = { module.exports = {
description: 'throws on double default exports', description: 'throws on double default exports',
error: err => { error: {
assert.equal( err.message, `Duplicate export 'default' (2:7) in ${path.resolve(__dirname, 'foo.js')}` ); 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;
^
`
} }
}; };

17
test/function/double-named-export/_config.js

@ -3,7 +3,20 @@ const assert = require( 'assert' );
module.exports = { module.exports = {
description: 'throws on duplicate named exports', description: 'throws on duplicate named exports',
error: err => { error: {
assert.equal( err.message, `Duplicate export 'foo' (3:9) in ${path.resolve(__dirname, 'foo.js')}` ); 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 };
^
`
} }
}; };

17
test/function/double-named-reexport/_config.js

@ -3,7 +3,20 @@ const assert = require( 'assert' );
module.exports = { module.exports = {
description: 'throws on duplicate named exports', description: 'throws on duplicate named exports',
error: err => { error: {
assert.equal( err.message, `Duplicate export 'foo' (3:9) in ${path.resolve(__dirname, 'foo.js')}` ); 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';
^
`
} }
}; };

19
test/function/export-not-at-top-level-fails/_config.js

@ -3,9 +3,20 @@ var assert = require( 'assert' );
module.exports = { module.exports = {
description: 'disallows non-top-level exports', description: 'disallows non-top-level exports',
error: function ( err ) { error: {
assert.equal( path.normalize(err.file), path.resolve( __dirname, 'main.js' ) ); code: 'PARSE_ERROR',
assert.deepEqual( err.loc, { line: 2, column: 2 }); message: `'import' and 'export' may only appear at the top level`,
assert.ok( /may only appear at the top level/.test( err.message ) ); pos: 19,
loc: {
file: path.resolve( __dirname, 'main.js' ),
line: 2,
column: 2
},
frame: `
1: function foo() {
2: export { foo };
^
3: }
`
} }
}; };

19
test/function/import-not-at-top-level-fails/_config.js

@ -3,9 +3,20 @@ var assert = require( 'assert' );
module.exports = { module.exports = {
description: 'disallows non-top-level imports', description: 'disallows non-top-level imports',
error: function ( err ) { error: {
assert.equal( path.normalize(err.file), path.resolve( __dirname, 'main.js' ) ); code: 'PARSE_ERROR',
assert.deepEqual( err.loc, { line: 2, column: 2 }); message: `'import' and 'export' may only appear at the top level`,
assert.ok( /may only appear at the top level/.test( err.message ) ); pos: 19,
loc: {
file: path.resolve( __dirname, 'main.js' ),
line: 2,
column: 2
},
frame: `
1: function foo() {
2: import foo from './foo.js';
^
3: }
`
} }
}; };

2
test/function/import-not-at-top-level-fails/main.js

@ -1,3 +1,3 @@
function foo() { function foo() {
import foo from './foo'; import foo from './foo.js';
} }

19
test/function/import-of-unexported-fails/_config.js

@ -1,8 +1,23 @@
var path = require( 'path' );
var assert = require( 'assert' ); var assert = require( 'assert' );
module.exports = { module.exports = {
description: 'marking an imported, but unexported, identifier should throw', description: 'marking an imported, but unexported, identifier should throw',
error: function ( err ) { error: {
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` ); 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`
} }
}; };

8
test/function/reports-syntax-error-locations/_config.js

@ -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 ) );
}
};

1
test/function/reports-syntax-error-locations/main.js

@ -1 +0,0 @@
var 42 = answer;

23
test/test.js

@ -79,6 +79,23 @@ function compareWarnings ( actual, expected ) {
); );
} }
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 () { describe( 'rollup', function () {
this.timeout( 10000 ); this.timeout( 10000 );
@ -324,7 +341,11 @@ describe( 'rollup', function () {
if ( unintendedError ) throw unintendedError; if ( unintendedError ) throw unintendedError;
}, err => { }, err => {
if ( config.error ) { if ( config.error ) {
config.error( err ); if ( typeof config.error === 'object' ) {
compareError( err, config.error );
} else {
config.error( err );
}
} else { } else {
throw err; throw err;
} }

Loading…
Cancel
Save