mirror of https://github.com/lukechilds/rollup.git
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.
431 lines
13 KiB
431 lines
13 KiB
import { Bundle as MagicStringBundle } from 'magic-string';
|
|
import first from './utils/first.js';
|
|
import { blank, forOwn, 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, makeOnwarn, resolveId } from './utils/defaults.js';
|
|
import getExportMode from './utils/getExportMode.js';
|
|
import getIndentString from './utils/getIndentString.js';
|
|
import { unixizePath } from './utils/normalizePlatform.js';
|
|
import { mapSequence } from './utils/promise.js';
|
|
import transform from './utils/transform.js';
|
|
import transformBundle from './utils/transformBundle.js';
|
|
import collapseSourcemaps from './utils/collapseSourcemaps.js';
|
|
import SOURCEMAPPING_URL from './utils/sourceMappingURL.js';
|
|
import callIfFunction from './utils/callIfFunction.js';
|
|
import { dirname, isRelative, isAbsolute, relative, resolve } from './utils/path.js';
|
|
|
|
export default class Bundle {
|
|
constructor ( options ) {
|
|
this.cachedModules = new Map();
|
|
if ( options.cache ) {
|
|
options.cache.modules.forEach( module => {
|
|
this.cachedModules.set( module.id, module );
|
|
});
|
|
}
|
|
|
|
this.plugins = ensureArray( options.plugins );
|
|
|
|
this.plugins.forEach( plugin => {
|
|
if ( plugin.options ) {
|
|
options = plugin.options( options ) || options;
|
|
}
|
|
});
|
|
|
|
this.entry = unixizePath( options.entry );
|
|
this.entryId = null;
|
|
this.entryModule = null;
|
|
|
|
this.treeshake = options.treeshake !== false;
|
|
|
|
this.resolveId = first(
|
|
[ id => this.isExternal( id ) ? false : null ]
|
|
.concat( 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.bundleTransformers = this.plugins
|
|
.map( plugin => plugin.transformBundle )
|
|
.filter( Boolean );
|
|
|
|
this.moduleById = new Map();
|
|
this.modules = [];
|
|
|
|
this.externalModules = [];
|
|
this.internalNamespaces = [];
|
|
|
|
this.assumedGlobals = blank();
|
|
|
|
if ( typeof options.external === 'function' ) {
|
|
this.isExternal = options.external;
|
|
} else {
|
|
const ids = ensureArray( options.external ).map( id => id.replace( /[\/\\]/g, '/' ) );
|
|
this.isExternal = id => ids.indexOf( id ) !== -1;
|
|
}
|
|
|
|
this.onwarn = options.onwarn || makeOnwarn();
|
|
|
|
// TODO strictly speaking, this only applies with non-ES6, non-default-only bundles
|
|
[ 'module', 'exports', '_interopDefault' ].forEach( global => this.assumedGlobals[ global ] = true );
|
|
|
|
this.varOrConst = options.preferConst ? 'const' : 'var';
|
|
}
|
|
|
|
build () {
|
|
// Phase 1 – discovery. We load the entry module and find which
|
|
// modules it imports, and import those, until we have all
|
|
// of the entry module's dependencies
|
|
return this.resolveId( this.entry, undefined )
|
|
.then( id => {
|
|
this.entryId = id;
|
|
return this.fetchModule( id, undefined );
|
|
})
|
|
.then( entryModule => {
|
|
this.entryModule = entryModule;
|
|
|
|
// Phase 2 – binding. We link references to their declarations
|
|
// to generate a complete picture of the bundle
|
|
this.modules.forEach( module => module.bindImportSpecifiers() );
|
|
this.modules.forEach( module => module.bindAliases() );
|
|
this.modules.forEach( module => module.bindReferences() );
|
|
|
|
// Phase 3 – marking. We 'run' each statement to see which ones
|
|
// need to be included in the generated bundle
|
|
|
|
// mark all export statements
|
|
entryModule.getExports().forEach( name => {
|
|
const declaration = entryModule.traceExport( name );
|
|
declaration.exportName = name;
|
|
|
|
declaration.use();
|
|
});
|
|
|
|
// mark statements that should appear in the bundle
|
|
let settled = false;
|
|
while ( !settled ) {
|
|
settled = true;
|
|
|
|
this.modules.forEach( module => {
|
|
if ( module.run( this.treeshake ) ) settled = false;
|
|
});
|
|
}
|
|
|
|
// Phase 4 – final preparation. We order the modules with an
|
|
// enhanced topological sort that accounts for cycles, then
|
|
// ensure that names are deconflicted throughout the bundle
|
|
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 );
|
|
|
|
// ensure we don't shadow named external imports, if
|
|
// we're creating an ES6 bundle
|
|
forOwn( module.declarations, ( declaration, name ) => {
|
|
declaration.setSafeName( getSafeName( name ) );
|
|
});
|
|
});
|
|
|
|
this.modules.forEach( module => {
|
|
forOwn( module.declarations, ( declaration, originalName ) => {
|
|
if ( declaration.isGlobal ) return;
|
|
|
|
if ( originalName === 'default' ) {
|
|
if ( declaration.original && !declaration.original.isReassigned ) return;
|
|
}
|
|
|
|
declaration.name = getSafeName( declaration.name );
|
|
});
|
|
});
|
|
}
|
|
|
|
fetchModule ( id, importer ) {
|
|
// short-circuit cycles
|
|
if ( this.moduleById.has( id ) ) return null;
|
|
this.moduleById.set( id, null );
|
|
|
|
return 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 => {
|
|
if ( typeof source === 'string' ) return source;
|
|
if ( source && typeof source === 'object' && source.code ) return source;
|
|
|
|
throw new Error( `Error loading ${id}: load hook should return a string, a { code, map } object, or nothing/null` );
|
|
})
|
|
.then( source => {
|
|
if ( typeof source === 'string' ) {
|
|
source = {
|
|
code: source,
|
|
ast: null
|
|
};
|
|
}
|
|
|
|
if ( this.cachedModules.has( id ) && this.cachedModules.get( id ).originalCode === source.code ) {
|
|
return this.cachedModules.get( id );
|
|
}
|
|
|
|
return 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.set( id, module );
|
|
|
|
return this.fetchAllDependencies( module ).then( () => module );
|
|
});
|
|
}
|
|
|
|
fetchAllDependencies ( module ) {
|
|
return mapSequence( module.sources, source => {
|
|
return this.resolveId( source, module.id )
|
|
.then( resolvedId => {
|
|
let externalName;
|
|
if ( resolvedId ) {
|
|
// If the `resolvedId` is supposed to be external, make it so.
|
|
externalName = resolvedId.replace( /[\/\\]/g, '/' );
|
|
} else if ( isRelative( source ) ) {
|
|
// This could be an external, relative dependency, based on the current module's parent dir.
|
|
externalName = resolve( module.id, '..', source );
|
|
}
|
|
const forcedExternal = externalName && this.isExternal( externalName );
|
|
|
|
if ( !resolvedId || forcedExternal ) {
|
|
let normalizedExternal = source;
|
|
|
|
if ( !forcedExternal ) {
|
|
if ( isRelative( source ) ) throw new Error( `Could not resolve ${source} from ${module.id}` );
|
|
if ( !this.isExternal( source ) ) this.onwarn( `Treating '${source}' as external dependency` );
|
|
} else if ( resolvedId ) {
|
|
if ( isRelative(resolvedId) || isAbsolute(resolvedId) ) {
|
|
// Try to deduce relative path from entry dir if resolvedId is defined as a relative path.
|
|
normalizedExternal = this.getPathRelativeToEntryDirname( resolvedId );
|
|
} else {
|
|
normalizedExternal = resolvedId;
|
|
}
|
|
}
|
|
module.resolvedIds[ source ] = normalizedExternal;
|
|
|
|
if ( !this.moduleById.has( normalizedExternal ) ) {
|
|
const module = new ExternalModule( normalizedExternal );
|
|
this.externalModules.push( module );
|
|
this.moduleById.set( normalizedExternal, 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 );
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
getPathRelativeToEntryDirname ( resolvedId ) {
|
|
// Get a path relative to the resolved entry directory
|
|
const entryDirname = dirname( this.entryId );
|
|
const relativeToEntry = relative( entryDirname, resolvedId );
|
|
|
|
if ( isRelative( relativeToEntry )) {
|
|
return relativeToEntry;
|
|
}
|
|
|
|
// The path is missing the `./` prefix
|
|
return `./${relativeToEntry}`;
|
|
}
|
|
|
|
render ( options = {} ) {
|
|
const format = options.format || 'es6';
|
|
|
|
// Determine export mode - 'default', 'named', 'none'
|
|
const exportMode = getExportMode( this, options.exports, options.moduleName );
|
|
|
|
let magicString = new MagicStringBundle({ 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 );
|
|
|
|
let code = magicString.toString();
|
|
let map = null;
|
|
let bundleSourcemapChain = [];
|
|
|
|
code = transformBundle( code, this.bundleTransformers, bundleSourcemapChain )
|
|
.replace( new RegExp( `\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'g' ), '' );
|
|
|
|
if ( options.sourceMap ) {
|
|
let file = options.sourceMapFile || options.dest;
|
|
if ( file ) file = resolve( typeof process !== 'undefined' ? process.cwd() : '', file );
|
|
|
|
map = magicString.generateMap({ file, includeContent: true });
|
|
|
|
if ( this.transformers.length || this.bundleTransformers.length ) {
|
|
map = collapseSourcemaps( map, usedModules, bundleSourcemapChain );
|
|
}
|
|
|
|
map.sources = map.sources.map( unixizePath );
|
|
}
|
|
|
|
return { code, map };
|
|
}
|
|
|
|
sort () {
|
|
let seen = {};
|
|
let hasCycles;
|
|
let ordered = [];
|
|
|
|
let stronglyDependsOn = blank();
|
|
let dependsOn = blank();
|
|
|
|
this.modules.forEach( module => {
|
|
stronglyDependsOn[ module.id ] = blank();
|
|
dependsOn[ module.id ] = blank();
|
|
});
|
|
|
|
this.modules.forEach( module => {
|
|
function processStrongDependency ( dependency ) {
|
|
if ( dependency === module || stronglyDependsOn[ module.id ][ dependency.id ] ) return;
|
|
|
|
stronglyDependsOn[ module.id ][ dependency.id ] = true;
|
|
dependency.strongDependencies.forEach( processStrongDependency );
|
|
}
|
|
|
|
function processDependency ( dependency ) {
|
|
if ( dependency === module || dependsOn[ module.id ][ dependency.id ] ) return;
|
|
|
|
dependsOn[ module.id ][ dependency.id ] = true;
|
|
dependency.dependencies.forEach( processDependency );
|
|
}
|
|
|
|
module.strongDependencies.forEach( processStrongDependency );
|
|
module.dependencies.forEach( processDependency );
|
|
});
|
|
|
|
const visit = module => {
|
|
if ( seen[ module.id ] ) {
|
|
hasCycles = true;
|
|
return;
|
|
}
|
|
|
|
seen[ module.id ] = true;
|
|
|
|
module.dependencies.forEach( visit );
|
|
ordered.push( module );
|
|
};
|
|
|
|
visit( this.entryModule );
|
|
|
|
if ( hasCycles ) {
|
|
ordered.forEach( ( a, i ) => {
|
|
for ( i += 1; i < ordered.length; i += 1 ) {
|
|
const b = ordered[i];
|
|
|
|
if ( stronglyDependsOn[ a.id ][ b.id ] ) {
|
|
// somewhere, there is a module that imports b before a. Because
|
|
// b imports a, a is placed before b. We need to find the module
|
|
// in question, so we can provide a useful error message
|
|
let parent = '[[unknown]]';
|
|
|
|
const findParent = module => {
|
|
if ( dependsOn[ module.id ][ a.id ] && dependsOn[ module.id ][ b.id ] ) {
|
|
parent = module.id;
|
|
} else {
|
|
for ( let i = 0; i < module.dependencies.length; i += 1 ) {
|
|
const dependency = module.dependencies[i];
|
|
if ( findParent( dependency ) ) return;
|
|
}
|
|
}
|
|
};
|
|
|
|
findParent( this.entryModule );
|
|
|
|
this.onwarn(
|
|
`Module ${a.id} may be unable to evaluate without ${b.id}, but is included first due to a cyclical dependency. Consider swapping the import statements in ${parent} to ensure correct ordering`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return ordered;
|
|
}
|
|
}
|
|
|