|
|
|
import { dirname } from './utils/path';
|
|
|
|
import { Promise } from 'sander';
|
|
|
|
import { parse } from 'acorn';
|
|
|
|
import MagicString from 'magic-string';
|
|
|
|
import Statement from './Statement';
|
|
|
|
import walk from './ast/walk';
|
|
|
|
import analyse from './ast/analyse';
|
|
|
|
import { blank, keys } from './utils/object';
|
|
|
|
import { first, sequence } from './utils/promise';
|
|
|
|
import { isImportDeclaration, isExportDeclaration } from './utils/map-helpers';
|
|
|
|
import getLocation from './utils/getLocation';
|
|
|
|
import makeLegalIdentifier from './utils/makeLegalIdentifier';
|
|
|
|
|
|
|
|
const emptyArrayPromise = Promise.resolve([]);
|
|
|
|
|
|
|
|
function deconflict ( name, names ) {
|
|
|
|
while ( name in names ) {
|
|
|
|
name = `_${name}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return name;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isEmptyExportedVarDeclaration ( node, module, allBundleExports, es6 ) {
|
|
|
|
if ( node.type !== 'VariableDeclaration' || node.declarations[0].init ) return false;
|
|
|
|
|
|
|
|
const name = node.declarations[0].id.name;
|
|
|
|
const canonicalName = module.getCanonicalName( name, es6 );
|
|
|
|
|
|
|
|
return canonicalName in allBundleExports;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class Module {
|
|
|
|
constructor ({ id, source, bundle }) {
|
|
|
|
this.source = source;
|
|
|
|
|
|
|
|
this.bundle = bundle;
|
|
|
|
this.id = id;
|
|
|
|
|
|
|
|
// By default, `id` is the filename. Custom resolvers and loaders
|
|
|
|
// can change that, but it makes sense to use it for the source filename
|
|
|
|
this.magicString = new MagicString( source, {
|
|
|
|
filename: id
|
|
|
|
});
|
|
|
|
|
|
|
|
this.suggestedNames = blank();
|
|
|
|
this.comments = [];
|
|
|
|
|
|
|
|
this.statements = this._parse();
|
|
|
|
|
|
|
|
// imports and exports, indexed by ID
|
|
|
|
this.imports = blank();
|
|
|
|
this.exports = blank();
|
|
|
|
|
|
|
|
this.exportAlls = blank();
|
|
|
|
|
|
|
|
// array of all-export sources
|
|
|
|
this.exportDelegates = [];
|
|
|
|
|
|
|
|
this.canonicalNames = blank();
|
|
|
|
|
|
|
|
this.definitions = blank();
|
|
|
|
this.definitionPromises = blank();
|
|
|
|
this.modifications = blank();
|
|
|
|
|
|
|
|
this.analyse();
|
|
|
|
}
|
|
|
|
|
|
|
|
addExport ( 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 );
|
|
|
|
const isAnonymous = /(?:Class|Function)Expression$/.test( node.declaration.type );
|
|
|
|
|
|
|
|
const declaredName = isDeclaration && node.declaration.id.name;
|
|
|
|
const identifier = node.declaration.type === 'Identifier' && node.declaration.name;
|
|
|
|
|
|
|
|
this.exports.default = {
|
|
|
|
statement,
|
|
|
|
name: 'default',
|
|
|
|
localName: declaredName || 'default',
|
|
|
|
declaredName,
|
|
|
|
identifier,
|
|
|
|
isDeclaration,
|
|
|
|
isAnonymous,
|
|
|
|
isModified: false // in case of `export default foo; foo = somethingElse`
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store `export * from '...'` statements in an array of delegates.
|
|
|
|
// When an unknown import is encountered, we see if one of them can satisfy it.
|
|
|
|
else {
|
|
|
|
this.exportDelegates.push({
|
|
|
|
statement,
|
|
|
|
source
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
addImport ( statement ) {
|
|
|
|
const node = statement.node;
|
|
|
|
const source = node.source.value;
|
|
|
|
|
|
|
|
node.specifiers.forEach( specifier => {
|
|
|
|
const isDefault = specifier.type === 'ImportDefaultSpecifier';
|
|
|
|
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
|
|
|
|
|
|
|
|
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 => {
|
|
|
|
( 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 ] ) {
|
|
|
|
this.bundle.assumedGlobals[ name ] = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
consolidateDependencies () {
|
|
|
|
let strongDependencies = blank();
|
|
|
|
|
|
|
|
this.statements.forEach( statement => {
|
|
|
|
if ( statement.isImportDeclaration && !statement.node.specifiers.length && !statement.module.isExternal ) {
|
|
|
|
// include module for its side-effects
|
|
|
|
strongDependencies[ statement.module.id ] = statement.module; // TODO is this right? `statement.module` should be `this`, surely?
|
|
|
|
}
|
|
|
|
|
|
|
|
keys( statement.stronglyDependsOn ).forEach( name => {
|
|
|
|
if ( statement.defines[ name ] ) return;
|
|
|
|
|
|
|
|
const exportAllDeclaration = this.exportAlls[ name ];
|
|
|
|
|
|
|
|
if ( exportAllDeclaration && exportAllDeclaration.module && !exportAllDeclaration.module.isExternal ) {
|
|
|
|
strongDependencies[ exportAllDeclaration.module.id ] = exportAllDeclaration.module;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const importDeclaration = this.imports[ name ];
|
|
|
|
|
|
|
|
if ( importDeclaration && importDeclaration.module && !importDeclaration.module.isExternal ) {
|
|
|
|
strongDependencies[ importDeclaration.module.id ] = importDeclaration.module;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
let weakDependencies = blank();
|
|
|
|
|
|
|
|
this.statements.forEach( statement => {
|
|
|
|
keys( statement.dependsOn ).forEach( name => {
|
|
|
|
if ( statement.defines[ name ] ) return;
|
|
|
|
|
|
|
|
const importDeclaration = this.imports[ name ];
|
|
|
|
|
|
|
|
if ( importDeclaration && importDeclaration.module && !importDeclaration.module.isExternal ) {
|
|
|
|
weakDependencies[ importDeclaration.module.id ] = importDeclaration.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 ];
|
|
|
|
|
|
|
|
// name was defined by another module
|
|
|
|
if ( importDeclaration ) {
|
|
|
|
const module = importDeclaration.module;
|
|
|
|
|
|
|
|
if ( module.isExternal ) return null;
|
|
|
|
|
|
|
|
const exportDeclaration = module.exports[ importDeclaration.name ];
|
|
|
|
return module.findDeclaration( exportDeclaration.localName );
|
|
|
|
}
|
|
|
|
|
|
|
|
// name was defined by this module, if any
|
|
|
|
let i = this.statements.length;
|
|
|
|
while ( i-- ) {
|
|
|
|
const declaration = this.statements[i].scope.declarations[ localName ];
|
|
|
|
if ( declaration ) {
|
|
|
|
return declaration;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
getCanonicalName ( localName, es6 ) {
|
|
|
|
// Special case
|
|
|
|
if ( localName === 'default' && ( this.exports.default.isModified || !this.suggestedNames.default ) ) {
|
|
|
|
let canonicalName = makeLegalIdentifier( this.id.replace( dirname( this.bundle.entryModule.id ) + '/', '' ).replace( /\.js$/, '' ) );
|
|
|
|
return deconflict( canonicalName, this.definitions );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( this.suggestedNames[ localName ] ) {
|
|
|
|
localName = this.suggestedNames[ localName ];
|
|
|
|
}
|
|
|
|
|
|
|
|
const id = localName + ( es6 ? '-es6' : '' ); // TODO ugh this seems like a terrible hack
|
|
|
|
|
|
|
|
if ( !this.canonicalNames[ id ] ) {
|
|
|
|
let canonicalName;
|
|
|
|
|
|
|
|
if ( 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 ];
|
|
|
|
|
|
|
|
// The export declaration of the particular name is known.
|
|
|
|
if (exportDeclaration) {
|
|
|
|
exporterLocalName = exportDeclaration.localName;
|
|
|
|
} else { // export * from '...'
|
|
|
|
exporterLocalName = importDeclaration.name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
canonicalName = module.getCanonicalName( exporterLocalName, es6 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
|
|
|
canonicalName = localName;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.canonicalNames[ id ] = canonicalName;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.canonicalNames[ id ];
|
|
|
|
}
|
|
|
|
|
|
|
|
mark ( name ) {
|
|
|
|
// shortcut cycles. TODO this won't work everywhere...
|
|
|
|
if ( this.definitionPromises[ name ] ) {
|
|
|
|
return emptyArrayPromise;
|
|
|
|
}
|
|
|
|
|
|
|
|
let promise;
|
|
|
|
|
|
|
|
// The definition for this name is in a different module
|
|
|
|
if ( this.imports[ name ] ) {
|
|
|
|
const importDeclaration = this.imports[ name ];
|
|
|
|
|
|
|
|
promise = this.bundle.fetchModule( importDeclaration.source, this.id )
|
|
|
|
.then( module => {
|
|
|
|
importDeclaration.module = module;
|
|
|
|
|
|
|
|
// suggest names. TODO should this apply to non default/* imports?
|
|
|
|
if ( importDeclaration.name === 'default' ) {
|
|
|
|
// TODO this seems ropey
|
|
|
|
const localName = importDeclaration.localName;
|
|
|
|
let suggestion = this.suggestedNames[ localName ] || localName;
|
|
|
|
|
|
|
|
// special case - the module has its own import by this name
|
|
|
|
while ( !module.isExternal && module.imports[ suggestion ] ) {
|
|
|
|
suggestion = `_${suggestion}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
module.suggestName( 'default', suggestion );
|
|
|
|
} else if ( importDeclaration.name === '*' ) {
|
|
|
|
const localName = importDeclaration.localName;
|
|
|
|
const suggestion = this.suggestedNames[ localName ] || localName;
|
|
|
|
module.suggestName( '*', suggestion );
|
|
|
|
module.suggestName( 'default', `${suggestion}__default` );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( module.isExternal ) {
|
|
|
|
if ( importDeclaration.name === 'default' ) {
|
|
|
|
module.needsDefault = true;
|
|
|
|
} else if ( importDeclaration.name === '*' ) {
|
|
|
|
module.needsAll = 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 );
|
|
|
|
}
|
|
|
|
|
|
|
|
return module.markAllStatements();
|
|
|
|
}
|
|
|
|
|
|
|
|
const exportDeclaration = module.exports[ importDeclaration.name ];
|
|
|
|
|
|
|
|
if ( !exportDeclaration ) {
|
|
|
|
const noExport = new Error( `Module ${module.id} does not export ${importDeclaration.name} (imported by ${this.id})` );
|
|
|
|
|
|
|
|
// See if there exists an export delegate that defines `name`.
|
|
|
|
return first( module.exportDelegates, noExport, declaration => {
|
|
|
|
return module.bundle.fetchModule( declaration.source, module.id ).then( submodule => {
|
|
|
|
declaration.module = submodule;
|
|
|
|
|
|
|
|
return submodule.mark( name ).then( result => {
|
|
|
|
if ( !result.length ) throw noExport;
|
|
|
|
|
|
|
|
// It's found! This module exports `name` through declaration.
|
|
|
|
// It is however not imported into this scope.
|
|
|
|
module.exportAlls[ name ] = declaration;
|
|
|
|
|
|
|
|
declaration.statement.dependsOn[ name ] =
|
|
|
|
declaration.statement.stronglyDependsOn[ name ] = result;
|
|
|
|
|
|
|
|
return result;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
exportDeclaration.isUsed = true;
|
|
|
|
return module.mark( 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.mark( this.exports.default.name );
|
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
|
|
|
let statement;
|
|
|
|
|
|
|
|
statement = name === 'default' ? this.exports.default.statement : this.definitions[ name ];
|
|
|
|
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
|
|
|
|
// declaration. Otherwise, the export declaration will always
|
|
|
|
// go at the end of the expansion, because the expansion of
|
|
|
|
// `foo` will include statements *after* the declaration
|
|
|
|
if ( name === 'default' && this.exports.default.identifier && this.exports.default.isModified ) {
|
|
|
|
const defaultExportStatement = this.exports.default.statement;
|
|
|
|
promise = promise.then( statements => {
|
|
|
|
// remove the default export statement...
|
|
|
|
// TODO could this be statements.pop()?
|
|
|
|
statements.splice( statements.indexOf( defaultExportStatement ), 1 );
|
|
|
|
|
|
|
|
let i = statements.length;
|
|
|
|
let inserted = false;
|
|
|
|
|
|
|
|
while ( i-- ) {
|
|
|
|
if ( statements[i].module === this && statements[i].index < defaultExportStatement.index ) {
|
|
|
|
statements.splice( i + 1, 0, defaultExportStatement );
|
|
|
|
inserted = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !inserted ) statements.push( statement );
|
|
|
|
return statements;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.definitionPromises[ name ] = promise || emptyArrayPromise;
|
|
|
|
return this.definitionPromises[ name ];
|
|
|
|
}
|
|
|
|
|
|
|
|
markAllStatements ( isEntryModule ) {
|
|
|
|
return sequence( this.statements, statement => {
|
|
|
|
if ( statement.isIncluded ) return; // TODO can this happen? probably not...
|
|
|
|
|
|
|
|
// 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.id )
|
|
|
|
.then( module => {
|
|
|
|
statement.module = module;
|
|
|
|
if ( module.isExternal ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return module.markAllStatements();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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.mark();
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// include everything else
|
|
|
|
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 })
|
|
|
|
});
|
|
|
|
} 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.forEach( 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 ) {
|
|
|
|
throw new Error( 'TODO' );
|
|
|
|
// 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 statement = new Statement( node, this, node.start, node.end ); // TODO should be comment start, comment end
|
|
|
|
|
|
|
|
statements.push( statement );
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
statements.forEach( ( statement, i ) => {
|
|
|
|
const nextStatement = statements[ i + 1 ];
|
|
|
|
statement.next = nextStatement ? nextStatement.start : statement.end;
|
|
|
|
});
|
|
|
|
|
|
|
|
return statements;
|
|
|
|
}
|
|
|
|
|
|
|
|
rename ( name, replacement ) {
|
|
|
|
// TODO again, hacky...
|
|
|
|
this.canonicalNames[ name ] = this.canonicalNames[ name + '-es6' ] = replacement;
|
|
|
|
}
|
|
|
|
|
|
|
|
render ( allBundleExports, format ) {
|
|
|
|
let magicString = this.magicString.clone();
|
|
|
|
|
|
|
|
let previousIndex = -1;
|
|
|
|
let previousMargin = 0;
|
|
|
|
|
|
|
|
this.statements.forEach( statement => {
|
|
|
|
if ( !statement.isIncluded ) {
|
|
|
|
magicString.remove( statement.start, statement.next );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// skip `export { foo, bar, baz }`
|
|
|
|
if ( statement.node.type === 'ExportNamedDeclaration' ) {
|
|
|
|
// skip `export { foo, bar, baz }`
|
|
|
|
if ( statement.node.specifiers.length ) {
|
|
|
|
magicString.remove( statement.start, statement.next );
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
// skip `export var foo;` if foo is exported
|
|
|
|
if ( isEmptyExportedVarDeclaration( statement.node.declaration, statement.module, allBundleExports, format === 'es6' ) ) {
|
|
|
|
magicString.remove( statement.start, statement.next );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// skip empty var declarations for exported bindings
|
|
|
|
// (otherwise we're left with `exports.foo;`, which is useless)
|
|
|
|
if ( isEmptyExportedVarDeclaration( statement.node, statement.module, allBundleExports, format === 'es6' ) ) {
|
|
|
|
magicString.remove( statement.start, statement.next );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let replacements = blank();
|
|
|
|
let bundleExports = blank();
|
|
|
|
|
|
|
|
keys( statement.dependsOn )
|
|
|
|
.concat( keys( statement.defines ) )
|
|
|
|
.forEach( name => {
|
|
|
|
const canonicalName = statement.module.getCanonicalName( name, format === 'es6' );
|
|
|
|
|
|
|
|
if ( allBundleExports[ canonicalName ] ) {
|
|
|
|
bundleExports[ name ] = replacements[ name ] = allBundleExports[ canonicalName ];
|
|
|
|
} else if ( name !== canonicalName ) {
|
|
|
|
replacements[ name ] = canonicalName;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
statement.replaceIdentifiers( magicString, replacements, bundleExports );
|
|
|
|
|
|
|
|
// modify exports as necessary
|
|
|
|
if ( statement.isExportDeclaration ) {
|
|
|
|
// remove `export` from `export var foo = 42`
|
|
|
|
if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.declaration.type === 'VariableDeclaration' ) {
|
|
|
|
magicString.remove( statement.node.start, statement.node.declaration.start );
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove `export` from `export class Foo {...}` or `export default Foo`
|
|
|
|
// TODO default exports need different treatment
|
|
|
|
else if ( statement.node.declaration.id ) {
|
|
|
|
magicString.remove( statement.node.start, statement.node.declaration.start );
|
|
|
|
}
|
|
|
|
|
|
|
|
else if ( statement.node.type === 'ExportDefaultDeclaration' ) {
|
|
|
|
const module = statement.module;
|
|
|
|
const canonicalName = module.getCanonicalName( 'default', format === 'es6' );
|
|
|
|
|
|
|
|
if ( statement.node.declaration.type === 'Identifier' && canonicalName === module.getCanonicalName( statement.node.declaration.name, format === 'es6' ) ) {
|
|
|
|
magicString.remove( statement.start, statement.next );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// anonymous functions should be converted into declarations
|
|
|
|
if ( statement.node.declaration.type === 'FunctionExpression' ) {
|
|
|
|
magicString.overwrite( statement.node.start, statement.node.declaration.start + 8, `function ${canonicalName}` );
|
|
|
|
} else {
|
|
|
|
magicString.overwrite( statement.node.start, statement.node.declaration.start, `var ${canonicalName} = ` );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
|
|
|
throw new Error( 'Unhandled export' );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// // ensure there is always a newline between statements, and add
|
|
|
|
// // additional newlines as necessary to reflect original source
|
|
|
|
// const minSeparation = ( statement.index !== previousIndex + 1 ) ? 3 : 2;
|
|
|
|
// const margin = Math.max( minSeparation, statement.margin[0], previousMargin );
|
|
|
|
// let newLines = new Array( margin ).join( '\n' );
|
|
|
|
//
|
|
|
|
// // add leading comments
|
|
|
|
// if ( statement.leadingComments.length ) {
|
|
|
|
// const commentBlock = newLines + statement.leadingComments.map( ({ separator, comment }) => {
|
|
|
|
// return separator + ( comment.block ?
|
|
|
|
// `/*${comment.text}*/` :
|
|
|
|
// `//${comment.text}` );
|
|
|
|
// }).join( '' );
|
|
|
|
//
|
|
|
|
// magicString.addSource( new MagicString( commentBlock ) );
|
|
|
|
// newLines = new Array( statement.margin[0] ).join( '\n' ); // TODO handle gaps between comment block and statement
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// // add the statement itself
|
|
|
|
// magicString.addSource({
|
|
|
|
// content: source,
|
|
|
|
// separator: newLines
|
|
|
|
// });
|
|
|
|
//
|
|
|
|
// // add trailing comments
|
|
|
|
// const comment = statement.trailingComment;
|
|
|
|
// if ( comment ) {
|
|
|
|
// const commentBlock = comment.block ?
|
|
|
|
// ` /*${comment.text}*/` :
|
|
|
|
// ` //${comment.text}`;
|
|
|
|
//
|
|
|
|
// magicString.append( commentBlock );
|
|
|
|
// }
|
|
|
|
//
|
|
|
|
// previousMargin = statement.margin[1];
|
|
|
|
// previousIndex = statement.index;
|
|
|
|
});
|
|
|
|
|
|
|
|
return magicString;
|
|
|
|
}
|
|
|
|
|
|
|
|
suggestName ( defaultOrBatch, suggestion ) {
|
|
|
|
// deconflict anonymous default exports with this module's definitions
|
|
|
|
const shouldDeconflict = this.exports.default && this.exports.default.isAnonymous;
|
|
|
|
|
|
|
|
if ( shouldDeconflict ) suggestion = deconflict( suggestion, this.definitions );
|
|
|
|
|
|
|
|
if ( !this.suggestedNames[ defaultOrBatch ] ) {
|
|
|
|
this.suggestedNames[ defaultOrBatch ] = makeLegalIdentifier( suggestion );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|