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.

214 lines
5.7 KiB

10 years ago
import { resolve } from 'path';
10 years ago
import { readFile } from 'sander';
import MagicString from 'magic-string';
import { keys, has } from './utils/object';
import { sequence } from './utils/promise';
import Module from './Module';
import ExternalModule from './ExternalModule';
import finalisers from './finalisers/index';
import replaceIdentifiers from './utils/replaceIdentifiers';
import makeLegalIdentifier from './utils/makeLegalIdentifier';
10 years ago
export default class Bundle {
constructor ( options ) {
this.base = options.base;
this.entryPath = resolve( this.base, options.entry );
10 years ago
this.entryModule = null;
this.modulePromises = {};
this.modules = {};
// this will store the top-level AST nodes we import
this.body = [];
// this will store per-module names, and enable deconflicting
this.bindingNames = {};
this.usedNames = {};
this.externalModules = [];
10 years ago
}
fetchModule ( path, id ) {
// TODO currently, we'll get different ExternalModule objects
// depending on where they're imported from...
if ( !has( this.modulePromises, path ) ) {
10 years ago
this.modulePromises[ path ] = readFile( path, { encoding: 'utf-8' })
.then( code => {
const module = new Module({
path,
code,
bundle: this
});
this.modules[ path ] = module;
return module;
}, err => {
if ( err.code === 'ENOENT' ) {
if ( id[0] === '.' ) {
// external modules can't have relative paths
throw err;
}
// most likely an external module
// TODO fire an event, or otherwise allow some way for
// users to control external modules better?
const module = new ExternalModule( id );
this.externalModules.push( module );
return module;
}
10 years ago
});
}
return this.modulePromises[ path ];
}
build () {
// bring in top-level AST nodes from the entry module
return this.fetchModule( this.entryPath )
.then( entryModule => {
this.entryModule = entryModule;
const importedNames = keys( entryModule.imports );
entryModule.definedNames
.concat( importedNames )
.forEach( name => {
this.usedNames[ name ] = true;
});
10 years ago
// pull in imports
return sequence( importedNames, name => {
10 years ago
return entryModule.define( name )
.then( nodes => {
this.body.push.apply( this.body, nodes );
});
10 years ago
})
.then( () => {
entryModule.ast.body.forEach( node => {
// exclude imports and exports, include everything else
if ( !/^(?:Im|Ex)port/.test( node.type ) ) {
this.body.push( node );
}
});
});
})
.then( () => {
this.deconflict();
10 years ago
});
}
deconflict () {
// TODO this probably needs to happen at generate time, since
// treatment of external modules differs between formats
// e.g. this...
//
// import { relative } from 'path'`;
// console.log( relative( 'foo', 'bar' ) );
//
// ...would look very similar when bundled as ES6, but in
// a CommonJS bundle would become this:
//
// var path = require( 'path' );
// console.log( path.relative( 'foo', 'bar' ) );
let definers = {};
let conflicts = {};
// Discover conflicts (i.e. two statements in separate modules both define `foo`)
this.body.forEach( statement => {
keys( statement._defines ).forEach( name => {
if ( has( definers, name ) ) {
conflicts[ name ] = true;
} else {
definers[ name ] = [];
}
// TODO in good js, there shouldn't be duplicate definitions
// per module... but some people write bad js
definers[ name ].push( statement._module );
});
});
// Assign names to external modules
this.externalModules.forEach( module => {
let name = makeLegalIdentifier( module.id );
while ( has( definers, name ) ) {
name = `_${name}`;
}
module.name = name;
});
// Rename conflicting identifiers so they can live in the same scope
keys( conflicts ).forEach( name => {
const modules = definers[ name ];
modules.pop(); // the module closest to the entryModule gets away with keeping things as they are
modules.forEach( module => {
module.rename( name, name + '$' + ~~( Math.random() * 100000 ) ); // TODO proper deconfliction mechanism
});
});
// Apply new names
this.body.forEach( statement => {
let replacements = {};
keys( statement._dependsOn )
.concat( keys( statement._defines ) )
.forEach( name => {
const canonicalName = statement._module.getCanonicalName( name );
if ( name !== canonicalName ) {
replacements[ name ] = canonicalName;
}
});
replaceIdentifiers( statement, statement._source, replacements );
});
10 years ago
}
generate ( options = {} ) {
let magicString = new MagicString.Bundle({ separator: '' });
this.entryModule.exportStatements.forEach( statement => {
if ( statement.specifiers.length ) {
// we don't need to include `export { foo }`, it's already handled
return;
}
if ( statement.declaration.type === 'VariableDeclaration' ) {
const declarator = statement.declaration.declarations[0];
statement._source.remove( statement.start, statement.declaration.start );
} else {
// TODO function, class declarations
}
// TODO are there situations where the export needs to be
// placed higher up, i.e. kept in situ? probably...
this.body.push( statement );
});
this.body.forEach( statement => {
magicString.addSource( statement._source );
});
const finalise = finalisers[ options.format || 'es6' ];
if ( !finalise ) {
throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` );
}
magicString = finalise( this, magicString, options );
10 years ago
return {
code: magicString.toString(),
map: magicString.generateMap({
})
};
10 years ago
}
}