You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

376 lines
10 KiB

10 years ago
import { relative } from 'path';
import { Promise } from 'sander';
10 years ago
import { parse } from 'acorn';
import MagicString from 'magic-string';
import Statement from './Statement';
10 years ago
import walk from './ast/walk';
import analyse from './ast/analyse';
import { blank, has, keys } from './utils/object';
import { sequence } from './utils/promise';
import { isImportDeclaration, isExportDeclaration } from './utils/map-helpers';
import getLocation from './utils/getLocation';
import makeLegalIdentifier from './utils/makeLegalIdentifier';
10 years ago
const emptyArrayPromise = Promise.resolve([]);
10 years ago
export default class Module {
constructor ({ path, source, bundle }) {
this.source = source;
10 years ago
this.bundle = bundle;
10 years ago
this.path = path;
10 years ago
this.magicString = new MagicString( source, {
filename: path
});
10 years ago
this.suggestedNames = {};
this.comments = [];
// Try to extract a list of top-level statements/declarations. If
// the parse fails, attach file info and abort
try {
const ast = parse( source, {
ecmaVersion: 6,
sourceType: 'module',
onComment: ( block, text, start, end ) => this.comments.push({ block, text, start, end })
});
10 years ago
walk( ast, {
enter: node => {
this.magicString.addSourcemapLocation( node.start );
this.magicString.addSourcemapLocation( node.end );
10 years ago
}
});
this.statements = ast.body.map( node => {
const magicString = this.magicString.snip( node.start, node.end );
return new Statement( node, magicString, this );
});
} catch ( err ) {
10 years ago
err.code = 'PARSE_ERROR';
err.file = path;
throw err;
}
this.importDeclarations = this.statements.filter( isImportDeclaration );
this.exportDeclarations = this.statements.filter( isExportDeclaration );
this.analyse();
}
analyse () {
// imports and exports, indexed by ID
10 years ago
this.imports = {};
this.exports = {};
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';
10 years ago
const localName = specifier.local.name;
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
if ( has( this.imports, localName ) ) {
const err = new Error( `Duplicated import '${localName}'` );
err.file = this.path;
err.loc = getLocation( this.source, specifier.start );
throw err;
}
10 years ago
this.imports[ localName ] = {
source,
name,
localName
};
});
});
this.exportDeclarations.forEach( statement => {
const node = statement.node;
const source = node.source && node.source.value;
// export default function foo () {}
// export default foo;
// export default 42;
if ( node.type === 'ExportDefaultDeclaration' ) {
const isDeclaration = /Declaration$/.test( node.declaration.type );
this.exports.default = {
statement,
name: 'default',
localName: isDeclaration ? node.declaration.id.name : 'default',
isDeclaration
};
10 years ago
}
// export { foo, bar, baz }
// export var foo = 42;
// export function foo () {}
else if ( node.type === 'ExportNamedDeclaration' ) {
if ( node.specifiers.length ) {
// export { foo, bar, baz }
node.specifiers.forEach( specifier => {
const localName = specifier.local.name;
const exportedName = specifier.exported.name;
this.exports[ exportedName ] = {
localName,
exportedName
};
// export { foo } from './foo';
if ( source ) {
this.imports[ localName ] = {
source,
localName,
name: localName
};
}
});
}
else {
let declaration = node.declaration;
let name;
if ( declaration.type === 'VariableDeclaration' ) {
// export var foo = 42
name = declaration.declarations[0].id.name;
} else {
// export function foo () {}
name = declaration.id.name;
}
this.exports[ name ] = {
statement,
localName: name,
expression: declaration
};
}
}
});
analyse( this.magicString, this );
this.canonicalNames = {};
this.definitions = {};
this.definitionPromises = {};
this.modifications = {};
this.statements.forEach( statement => {
Object.keys( statement.defines ).forEach( name => {
this.definitions[ name ] = statement;
});
Object.keys( statement.modifies ).forEach( name => {
if ( !has( this.modifications, name ) ) {
this.modifications[ name ] = [];
}
this.modifications[ name ].push( statement );
});
});
}
getCanonicalName ( localName ) {
if ( has( this.suggestedNames, localName ) ) {
localName = this.suggestedNames[ localName ];
}
if ( !has( this.canonicalNames, localName ) ) {
let canonicalName;
if ( has( this.imports, localName ) ) {
const importDeclaration = this.imports[ localName ];
const module = importDeclaration.module;
if ( importDeclaration.name === '*' ) {
canonicalName = module.suggestedNames[ '*' ];
} else {
let exporterLocalName;
if ( module.isExternal ) {
exporterLocalName = importDeclaration.name;
} else {
const exportDeclaration = module.exports[ importDeclaration.name ];
exporterLocalName = exportDeclaration.localName;
}
canonicalName = module.getCanonicalName( exporterLocalName );
}
}
else {
canonicalName = localName;
}
this.canonicalNames[ localName ] = canonicalName;
}
return this.canonicalNames[ localName ];
}
define ( name ) {
// shortcut cycles. TODO this won't work everywhere...
if ( has( this.definitionPromises, name ) ) {
return emptyArrayPromise;
}
let promise;
// The definition for this name is in a different module
if ( has( this.imports, name ) ) {
const importDeclaration = this.imports[ name ];
promise = this.bundle.fetchModule( importDeclaration.source, this.path )
.then( module => {
importDeclaration.module = module;
// suggest names. TODO should this apply to non default/* imports?
if ( importDeclaration.name === 'default' ) {
10 years ago
// TODO this seems ropey
const localName = importDeclaration.localName;
let suggestion = has( this.suggestedNames, localName ) ? this.suggestedNames[ localName ] : localName;
// special case - the module has its own import by this name
while ( !module.isExternal && has( module.imports, suggestion ) ) {
suggestion = `_${suggestion}`;
}
10 years ago
module.suggestName( 'default', suggestion );
} else if ( importDeclaration.name === '*' ) {
const localName = importDeclaration.localName;
const suggestion = has( this.suggestedNames, localName ) ? this.suggestedNames[ localName ] : localName;
module.suggestName( '*', suggestion );
module.suggestName( 'default', `${suggestion}__default` );
}
if ( module.isExternal ) {
if ( importDeclaration.name === 'default' ) {
module.needsDefault = true;
} else {
module.needsNamed = true;
}
module.importedByBundle.push( importDeclaration );
return emptyArrayPromise;
}
if ( importDeclaration.name === '*' ) {
// we need to create an internal namespace
if ( !~this.bundle.internalNamespaceModules.indexOf( module ) ) {
this.bundle.internalNamespaceModules.push( module );
}
10 years ago
return module.expandAllStatements();
}
const exportDeclaration = module.exports[ importDeclaration.name ];
if ( !exportDeclaration ) {
throw new Error( `Module ${module.path} does not export ${importDeclaration.name} (imported by ${this.path})` );
}
return module.define( exportDeclaration.localName );
});
}
// The definition is in this 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 );
}
10 years ago
else {
let statement;
10 years ago
if ( name === 'default' ) {
statement = this.exports.default.statement;
10 years ago
} else {
statement = this.definitions[ name ];
}
if ( statement && !statement.isIncluded ) {
promise = statement.expand();
10 years ago
}
}
10 years ago
this.definitionPromises[ name ] = promise || emptyArrayPromise;
return this.definitionPromises[ name ];
}
expandAllStatements ( isEntryModule ) {
10 years ago
let allStatements = [];
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;
}
// skip import declarations...
if ( statement.isImportDeclaration ) {
// ...unless they're empty, in which case assume we're importing them for the side-effects
// THIS IS NOT FOOLPROOF. Probably need /*rollup: include */ or similar
if ( !statement.node.specifiers.length ) {
return this.bundle.fetchModule( statement.node.source.value, this.path )
.then( module => {
statement.module = module; // TODO what is this for? what does it do? why not _module?
return module.expandAllStatements();
})
.then( statements => {
allStatements.push.apply( allStatements, statements );
});
}
return;
}
// skip `export { foo, bar, baz }`...
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;
}
// include everything else
return statement.expand().then( statements => {
allStatements.push.apply( allStatements, statements );
});
10 years ago
}).then( () => {
return allStatements;
});
}
rename ( name, replacement ) {
this.canonicalNames[ name ] = replacement;
}
suggestName ( exportName, suggestion ) {
if ( !this.suggestedNames[ exportName ] ) {
this.suggestedNames[ exportName ] = makeLegalIdentifier( suggestion );
}
}
}