UNPKG

rollup

Version:

Next-generation ES6 module bundler

329 lines (254 loc) 8.99 kB
import Promise from 'es6-promise/lib/es6-promise/promise.js'; import MagicString from 'magic-string'; import first from './utils/first.js'; import { blank, keys } from './utils/object.js'; import Module from './Module.js'; import ExternalModule from './ExternalModule.js'; import finalisers from './finalisers/index.js'; import ensureArray from './utils/ensureArray.js'; import { load, onwarn, resolveId } from './utils/defaults.js'; import getExportMode from './utils/getExportMode.js'; import getIndentString from './utils/getIndentString.js'; import { unixizePath } from './utils/normalizePlatform.js'; import transform from './utils/transform.js'; import collapseSourcemaps from './utils/collapseSourcemaps.js'; import callIfFunction from './utils/callIfFunction.js'; export default class Bundle { constructor ( options ) { this.entry = options.entry; this.entryModule = null; this.plugins = ensureArray( options.plugins ); this.resolveId = first( this.plugins .map( plugin => plugin.resolveId ) .filter( Boolean ) .concat( resolveId ) ); this.load = first( this.plugins .map( plugin => plugin.load ) .filter( Boolean ) .concat( load ) ); this.transformers = this.plugins .map( plugin => plugin.transform ) .filter( Boolean ); this.pending = blank(); this.moduleById = blank(); this.modules = []; this.externalModules = []; this.internalNamespaces = []; this.assumedGlobals = blank(); this.external = options.external || []; this.onwarn = options.onwarn || onwarn; // TODO strictly speaking, this only applies with non-ES6, non-default-only bundles [ 'module', 'exports' ].forEach( global => this.assumedGlobals[ global ] = true ); } build () { return Promise.resolve( this.resolveId( this.entry, undefined ) ) .then( id => this.fetchModule( id, undefined ) ) .then( entryModule => { this.entryModule = entryModule; this.modules.forEach( module => module.bindImportSpecifiers() ); this.modules.forEach( module => module.bindAliases() ); this.modules.forEach( module => module.bindReferences() ); // mark all export statements entryModule.getExports().forEach( name => { const declaration = entryModule.traceExport( name ); declaration.isExported = true; declaration.use(); }); let settled = false; while ( !settled ) { settled = true; this.modules.forEach( module => { if ( module.markAllSideEffects() ) settled = false; }); } this.orderedModules = this.sort(); this.deconflict(); }); } deconflict () { let used = blank(); // ensure no conflicts with globals keys( this.assumedGlobals ).forEach( name => used[ name ] = 1 ); function getSafeName ( name ) { while ( used[ name ] ) { name += `$${used[name]++}`; } used[ name ] = 1; return name; } this.externalModules.forEach( module => { module.name = getSafeName( module.name ); }); this.modules.forEach( module => { keys( module.declarations ).forEach( originalName => { const declaration = module.declarations[ originalName ]; if ( originalName === 'default' ) { if ( declaration.original && !declaration.original.isReassigned ) return; } declaration.name = getSafeName( declaration.name ); }); }); } fetchModule ( id, importer ) { // short-circuit cycles if ( this.pending[ id ] ) return null; this.pending[ id ] = true; return Promise.resolve( this.load( id ) ) .catch( err => { let msg = `Could not load ${id}`; if ( importer ) msg += ` (imported by ${importer})`; msg += `: ${err.message}`; throw new Error( msg ); }) .then( source => transform( source, id, this.transformers ) ) .then( source => { const { code, originalCode, ast, sourceMapChain } = source; const module = new Module({ id, code, originalCode, ast, sourceMapChain, bundle: this }); this.modules.push( module ); this.moduleById[ id ] = module; return this.fetchAllDependencies( module ).then( () => module ); }); } fetchAllDependencies ( module ) { const promises = module.dependencies.map( source => { return Promise.resolve( this.resolveId( source, module.id ) ) .then( resolvedId => { if ( !resolvedId ) { if ( !~this.external.indexOf( source ) ) this.onwarn( `Treating '${source}' as external dependency` ); module.resolvedIds[ source ] = source; if ( !this.moduleById[ source ] ) { const module = new ExternalModule( source ); this.externalModules.push( module ); this.moduleById[ source ] = module; } } else { if ( resolvedId === module.id ) { throw new Error( `A module cannot import itself (${resolvedId})` ); } module.resolvedIds[ source ] = resolvedId; return this.fetchModule( resolvedId, module.id ); } }); }); return Promise.all( promises ); } render ( options = {} ) { const format = options.format || 'es6'; // Determine export mode - 'default', 'named', 'none' const exportMode = getExportMode( this, options.exports ); let magicString = new MagicString.Bundle({ separator: '\n\n' }); let usedModules = []; this.orderedModules.forEach( module => { const source = module.render( format === 'es6' ); if ( source.toString().length ) { magicString.addSource( source ); usedModules.push( module ); } }); const intro = [ options.intro ] .concat( this.plugins.map( plugin => plugin.intro && plugin.intro() ) ) .filter( Boolean ) .join( '\n\n' ); if ( intro ) magicString.prepend( intro + '\n' ); if ( options.outro ) magicString.append( '\n' + options.outro ); 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( ', ' )}` ); magicString = finalise( this, magicString.trim(), { exportMode, indentString }, options ); const banner = [ options.banner ] .concat( this.plugins.map( plugin => plugin.banner ) ) .map( callIfFunction ) .filter( Boolean ) .join( '\n' ); const footer = [ options.footer ] .concat( this.plugins.map( plugin => plugin.footer ) ) .map( callIfFunction ) .filter( Boolean ) .join( '\n' ); if ( banner ) magicString.prepend( banner + '\n' ); if ( footer ) magicString.append( '\n' + footer ); const code = magicString.toString(); let map = null; if ( options.sourceMap ) { const file = options.sourceMapFile || options.dest; map = magicString.generateMap({ includeContent: true, file // TODO }); if ( this.transformers.length ) map = collapseSourcemaps( map, usedModules ); map.sources = map.sources.map( unixizePath ); } return { code, map }; } sort () { let seen = {}; let ordered = []; let hasCycles; let strongDeps = {}; let stronglyDependsOn = {}; function visit ( module ) { if ( seen[ module.id ] ) return; seen[ module.id ] = true; const { strongDependencies, weakDependencies } = module.consolidateDependencies(); strongDeps[ module.id ] = []; stronglyDependsOn[ module.id ] = {}; keys( strongDependencies ).forEach( id => { const imported = strongDependencies[ id ]; strongDeps[ module.id ].push( imported ); if ( seen[ id ] ) { // we need to prevent an infinite loop, and note that // we need to check for strong/weak dependency relationships hasCycles = true; return; } visit( imported ); }); keys( weakDependencies ).forEach( id => { const imported = weakDependencies[ id ]; if ( seen[ id ] ) { // we need to prevent an infinite loop, and note that // we need to check for strong/weak dependency relationships hasCycles = true; return; } visit( imported ); }); // add second (and third...) order dependencies function addStrongDependencies ( dependency ) { if ( stronglyDependsOn[ module.id ][ dependency.id ] ) return; stronglyDependsOn[ module.id ][ dependency.id ] = true; strongDeps[ dependency.id ].forEach( addStrongDependencies ); } strongDeps[ module.id ].forEach( addStrongDependencies ); ordered.push( module ); } this.modules.forEach( visit ); if ( hasCycles ) { let unordered = ordered; ordered = []; // unordered is actually semi-ordered, as [ fewer dependencies ... more dependencies ] unordered.forEach( module => { // ensure strong dependencies of `module` that don't strongly depend on `module` go first strongDeps[ module.id ].forEach( place ); function place ( dep ) { if ( !stronglyDependsOn[ dep.id ][ module.id ] && !~ordered.indexOf( dep ) ) { strongDeps[ dep.id ].forEach( place ); ordered.push( dep ); } } if ( !~ordered.indexOf( module ) ) { ordered.push( module ); } }); } return ordered; } }