|
|
@ -37,99 +37,22 @@ export default class Module { |
|
|
|
this.suggestedNames = blank(); |
|
|
|
this.comments = []; |
|
|
|
|
|
|
|
// Try to extract a list of top-level statements/declarations. If
|
|
|
|
// the parse fails, attach file info and abort
|
|
|
|
let ast; |
|
|
|
|
|
|
|
try { |
|
|
|
ast = parse( source, { |
|
|
|
ecmaVersion: 6, |
|
|
|
sourceType: 'module', |
|
|
|
onComment: ( block, text, start, end ) => this.comments.push({ block, text, start, end }) |
|
|
|
}); |
|
|
|
} catch ( err ) { |
|
|
|
err.code = 'PARSE_ERROR'; |
|
|
|
err.file = id; // see above - not necessarily true, but true enough
|
|
|
|
throw err; |
|
|
|
} |
|
|
|
|
|
|
|
walk( ast, { |
|
|
|
enter: node => { |
|
|
|
this.magicString.addSourcemapLocation( node.start ); |
|
|
|
this.magicString.addSourcemapLocation( node.end ); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
this.statements = []; |
|
|
|
|
|
|
|
ast.body.map( node => { |
|
|
|
// special case - top-level var declarations with multiple declarators
|
|
|
|
// should be split up. Otherwise, we may end up including code we
|
|
|
|
// don't need, just because an unwanted declarator is included
|
|
|
|
if ( node.type === 'VariableDeclaration' && node.declarations.length > 1 ) { |
|
|
|
node.declarations.forEach( declarator => { |
|
|
|
const magicString = this.magicString.snip( declarator.start, declarator.end ).trim(); |
|
|
|
magicString.prepend( `${node.kind} ` ).append( ';' ); |
|
|
|
|
|
|
|
const syntheticNode = { |
|
|
|
type: 'VariableDeclaration', |
|
|
|
kind: node.kind, |
|
|
|
start: node.start, |
|
|
|
end: node.end, |
|
|
|
declarations: [ declarator ] |
|
|
|
}; |
|
|
|
|
|
|
|
const statement = new Statement( syntheticNode, magicString, this, this.statements.length ); |
|
|
|
this.statements.push( statement ); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
else { |
|
|
|
const magicString = this.magicString.snip( node.start, node.end ).trim(); |
|
|
|
const statement = new Statement( node, magicString, this, this.statements.length ); |
|
|
|
|
|
|
|
this.statements.push( statement ); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
this.importDeclarations = this.statements.filter( isImportDeclaration ); |
|
|
|
this.exportDeclarations = this.statements.filter( isExportDeclaration ); |
|
|
|
|
|
|
|
this.analyse(); |
|
|
|
} |
|
|
|
this.statements = this._parse(); |
|
|
|
|
|
|
|
analyse () { |
|
|
|
// imports and exports, indexed by ID
|
|
|
|
this.imports = blank(); |
|
|
|
this.exports = blank(); |
|
|
|
|
|
|
|
this.importDeclarations.forEach( statement => { |
|
|
|
const node = statement.node; |
|
|
|
const source = node.source.value; |
|
|
|
|
|
|
|
node.specifiers.forEach( specifier => { |
|
|
|
const isDefault = specifier.type === 'ImportDefaultSpecifier'; |
|
|
|
const isNamespace = specifier.type === 'ImportNamespaceSpecifier'; |
|
|
|
this.canonicalNames = blank(); |
|
|
|
|
|
|
|
const localName = specifier.local.name; |
|
|
|
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name; |
|
|
|
this.definitions = blank(); |
|
|
|
this.definitionPromises = blank(); |
|
|
|
this.modifications = blank(); |
|
|
|
|
|
|
|
if ( this.imports[ localName ] ) { |
|
|
|
const err = new Error( `Duplicated import '${localName}'` ); |
|
|
|
err.file = this.id; |
|
|
|
err.loc = getLocation( this.source, specifier.start ); |
|
|
|
throw err; |
|
|
|
this.analyse(); |
|
|
|
} |
|
|
|
|
|
|
|
this.imports[ localName ] = { |
|
|
|
source, |
|
|
|
name, |
|
|
|
localName |
|
|
|
}; |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
this.exportDeclarations.forEach( statement => { |
|
|
|
addExport ( statement ) { |
|
|
|
const node = statement.node; |
|
|
|
const source = node.source && node.source.value; |
|
|
|
|
|
|
@ -201,30 +124,56 @@ export default class Module { |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
analyse( this.magicString, this ); |
|
|
|
addImport ( statement ) { |
|
|
|
const node = statement.node; |
|
|
|
const source = node.source.value; |
|
|
|
|
|
|
|
this.canonicalNames = blank(); |
|
|
|
node.specifiers.forEach( specifier => { |
|
|
|
const isDefault = specifier.type === 'ImportDefaultSpecifier'; |
|
|
|
const isNamespace = specifier.type === 'ImportNamespaceSpecifier'; |
|
|
|
|
|
|
|
this.definitions = blank(); |
|
|
|
this.definitionPromises = blank(); |
|
|
|
this.modifications = blank(); |
|
|
|
const localName = specifier.local.name; |
|
|
|
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name; |
|
|
|
|
|
|
|
if ( this.imports[ localName ] ) { |
|
|
|
const err = new Error( `Duplicated import '${localName}'` ); |
|
|
|
err.file = this.id; |
|
|
|
err.loc = getLocation( this.source, specifier.start ); |
|
|
|
throw err; |
|
|
|
} |
|
|
|
|
|
|
|
this.imports[ localName ] = { |
|
|
|
source, |
|
|
|
name, |
|
|
|
localName |
|
|
|
}; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
analyse () { |
|
|
|
// discover this module's imports and exports
|
|
|
|
this.statements.forEach( statement => { |
|
|
|
if ( isImportDeclaration( statement ) ) this.addImport( statement ); |
|
|
|
else if ( isExportDeclaration( statement ) ) this.addExport( statement ); |
|
|
|
}); |
|
|
|
|
|
|
|
analyse( this.magicString, this ); |
|
|
|
|
|
|
|
// consolidate names that are defined/modified in this module
|
|
|
|
this.statements.forEach( statement => { |
|
|
|
keys( statement.defines ).forEach( name => { |
|
|
|
this.definitions[ name ] = statement; |
|
|
|
}); |
|
|
|
|
|
|
|
keys( statement.modifies ).forEach( name => { |
|
|
|
if ( !this.modifications[ name ] ) { |
|
|
|
this.modifications[ name ] = []; |
|
|
|
} |
|
|
|
|
|
|
|
this.modifications[ name ].push( statement ); |
|
|
|
( this.modifications[ name ] || ( this.modifications[ name ] = [] ) ).push( statement ); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
// if names are referenced that are neither defined nor imported
|
|
|
|
// in this module, we assume that they're globals
|
|
|
|
this.statements.forEach( statement => { |
|
|
|
keys( statement.dependsOn ).forEach( name => { |
|
|
|
if ( !this.definitions[ name ] && !this.imports[ name ] ) { |
|
|
@ -271,6 +220,21 @@ export default class Module { |
|
|
|
return { strongDependencies, weakDependencies }; |
|
|
|
} |
|
|
|
|
|
|
|
findDefiningStatement ( name ) { |
|
|
|
if ( this.definitions[ name ] ) return this.definitions[ name ]; |
|
|
|
|
|
|
|
// TODO what about `default`/`*`?
|
|
|
|
|
|
|
|
const importDeclaration = this.imports[ name ]; |
|
|
|
if ( !importDeclaration ) return null; |
|
|
|
|
|
|
|
return Promise.resolve( importDeclaration.module || this.bundle.fetchModule( importDeclaration.source, this.id ) ) |
|
|
|
.then( module => { |
|
|
|
importDeclaration.module = module; |
|
|
|
return module.findDefiningStatement( name ); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
findDeclaration ( localName ) { |
|
|
|
const importDeclaration = this.imports[ localName ]; |
|
|
|
|
|
|
@ -340,7 +304,7 @@ export default class Module { |
|
|
|
return this.canonicalNames[ localName ]; |
|
|
|
} |
|
|
|
|
|
|
|
define ( name ) { |
|
|
|
mark ( name ) { |
|
|
|
// shortcut cycles. TODO this won't work everywhere...
|
|
|
|
if ( this.definitionPromises[ name ] ) { |
|
|
|
return emptyArrayPromise; |
|
|
@ -392,7 +356,7 @@ export default class Module { |
|
|
|
this.bundle.internalNamespaceModules.push( module ); |
|
|
|
} |
|
|
|
|
|
|
|
return module.expandAllStatements(); |
|
|
|
return module.markAllStatements(); |
|
|
|
} |
|
|
|
|
|
|
|
const exportDeclaration = module.exports[ importDeclaration.name ]; |
|
|
@ -401,7 +365,7 @@ export default class Module { |
|
|
|
throw new Error( `Module ${module.id} does not export ${importDeclaration.name} (imported by ${this.id})` ); |
|
|
|
} |
|
|
|
|
|
|
|
return module.define( exportDeclaration.localName ); |
|
|
|
return module.mark( exportDeclaration.localName ); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
@ -409,14 +373,14 @@ export default class Module { |
|
|
|
else if ( name === 'default' && this.exports.default.isDeclaration ) { |
|
|
|
// We have something like `export default foo` - so we just start again,
|
|
|
|
// searching for `foo` instead of default
|
|
|
|
promise = this.define( this.exports.default.name ); |
|
|
|
promise = this.mark( this.exports.default.name ); |
|
|
|
} |
|
|
|
|
|
|
|
else { |
|
|
|
let statement; |
|
|
|
|
|
|
|
statement = name === 'default' ? this.exports.default.statement : this.definitions[ name ]; |
|
|
|
promise = statement && !statement.isIncluded ? statement.expand() : emptyArrayPromise; |
|
|
|
promise = statement && !statement.isIncluded ? statement.mark() : emptyArrayPromise; |
|
|
|
|
|
|
|
// Special case - `export default foo; foo += 1` - need to be
|
|
|
|
// vigilant about maintaining the correct order of the export
|
|
|
@ -451,22 +415,9 @@ export default class Module { |
|
|
|
return this.definitionPromises[ name ]; |
|
|
|
} |
|
|
|
|
|
|
|
expandAllStatements ( isEntryModule ) { |
|
|
|
let allStatements = []; |
|
|
|
|
|
|
|
markAllStatements ( isEntryModule ) { |
|
|
|
return sequence( this.statements, statement => { |
|
|
|
// A statement may have already been included, in which case we need to
|
|
|
|
// curb rollup's enthusiasm and move it down here. It remains to be seen
|
|
|
|
// if this approach is bulletproof
|
|
|
|
if ( statement.isIncluded ) { |
|
|
|
const index = allStatements.indexOf( statement ); |
|
|
|
if ( ~index ) { |
|
|
|
allStatements.splice( index, 1 ); |
|
|
|
allStatements.push( statement ); |
|
|
|
} |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
if ( statement.isIncluded ) return; // TODO can this happen? probably not...
|
|
|
|
|
|
|
|
// skip import declarations...
|
|
|
|
if ( statement.isImportDeclaration ) { |
|
|
@ -476,10 +427,7 @@ export default class Module { |
|
|
|
return this.bundle.fetchModule( statement.node.source.value, this.id ) |
|
|
|
.then( module => { |
|
|
|
statement.module = module; |
|
|
|
return module.expandAllStatements(); |
|
|
|
}) |
|
|
|
.then( statements => { |
|
|
|
allStatements.push.apply( allStatements, statements ); |
|
|
|
return module.markAllStatements(); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
@ -490,21 +438,75 @@ export default class Module { |
|
|
|
if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) { |
|
|
|
// ...but ensure they are defined, if this is the entry module
|
|
|
|
if ( isEntryModule ) { |
|
|
|
return statement.expand().then( statements => { |
|
|
|
allStatements.push.apply( allStatements, statements ); |
|
|
|
}); |
|
|
|
return statement.mark(); |
|
|
|
} |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// include everything else
|
|
|
|
return statement.expand().then( statements => { |
|
|
|
allStatements.push.apply( allStatements, statements ); |
|
|
|
return statement.mark(); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// TODO rename this to parse, once https://github.com/rollup/rollup/issues/42 is fixed
|
|
|
|
_parse () { |
|
|
|
// Try to extract a list of top-level statements/declarations. If
|
|
|
|
// the parse fails, attach file info and abort
|
|
|
|
let ast; |
|
|
|
|
|
|
|
try { |
|
|
|
ast = parse( this.source, { |
|
|
|
ecmaVersion: 6, |
|
|
|
sourceType: 'module', |
|
|
|
onComment: ( block, text, start, end ) => this.comments.push({ block, text, start, end }) |
|
|
|
}); |
|
|
|
}).then( () => { |
|
|
|
return allStatements; |
|
|
|
} catch ( err ) { |
|
|
|
err.code = 'PARSE_ERROR'; |
|
|
|
err.file = this.id; // see above - not necessarily true, but true enough
|
|
|
|
throw err; |
|
|
|
} |
|
|
|
|
|
|
|
walk( ast, { |
|
|
|
enter: node => { |
|
|
|
this.magicString.addSourcemapLocation( node.start ); |
|
|
|
this.magicString.addSourcemapLocation( node.end ); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
let statements = []; |
|
|
|
|
|
|
|
ast.body.map( node => { |
|
|
|
// special case - top-level var declarations with multiple declarators
|
|
|
|
// should be split up. Otherwise, we may end up including code we
|
|
|
|
// don't need, just because an unwanted declarator is included
|
|
|
|
if ( node.type === 'VariableDeclaration' && node.declarations.length > 1 ) { |
|
|
|
node.declarations.forEach( declarator => { |
|
|
|
const magicString = this.magicString.snip( declarator.start, declarator.end ).trim(); |
|
|
|
magicString.prepend( `${node.kind} ` ).append( ';' ); |
|
|
|
|
|
|
|
const syntheticNode = { |
|
|
|
type: 'VariableDeclaration', |
|
|
|
kind: node.kind, |
|
|
|
start: node.start, |
|
|
|
end: node.end, |
|
|
|
declarations: [ declarator ] |
|
|
|
}; |
|
|
|
|
|
|
|
const statement = new Statement( syntheticNode, magicString, this, statements.length ); |
|
|
|
statements.push( statement ); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
else { |
|
|
|
const magicString = this.magicString.snip( node.start, node.end ).trim(); |
|
|
|
const statement = new Statement( node, magicString, this, statements.length ); |
|
|
|
|
|
|
|
statements.push( statement ); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
return statements; |
|
|
|
} |
|
|
|
|
|
|
|
rename ( name, replacement ) { |
|
|
|