import { basename, extname } from './utils/path'; import { parse } from 'acorn'; import MagicString from 'magic-string'; import Statement from './Statement'; import walk from './ast/walk'; import { blank, keys } from './utils/object'; import getLocation from './utils/getLocation'; import makeLegalIdentifier from './utils/makeLegalIdentifier'; import SOURCEMAPPING_URL from './utils/sourceMappingURL'; function removeSourceMappingURLComments ( source, magicString ) { const SOURCEMAPPING_URL_PATTERN = new RegExp( `\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'g' ); let match; while ( match = SOURCEMAPPING_URL_PATTERN.exec( source ) ) { magicString.remove( match.index, match.index + match[0].length ); } } function assign ( target, source ) { for ( let key in source ) target[ key ] = source[ key ]; } class Id { constructor ( module, name, statement ) { this.originalName = this.name = name; this.module = module; this.statement = statement; this.modifierStatements = []; // modifiers this.isUsed = false; } mark () { this.isUsed = true; this.statement.mark(); this.modifierStatements.forEach( stmt => stmt.mark() ); } } class LateBoundIdPlaceholder { constructor ( module, name ) { this.module = module; this.name = name; this.placeholder = true; } mark () { throw new Error(`The imported name "${this.name}" is never exported by "${this.module.id}".`); } } export default class Module { constructor ({ id, source, ast, bundle }) { this.source = source; this.bundle = bundle; this.id = id; this.module = this; this.isModule = true; // Implement Identifier interface. this.name = makeLegalIdentifier( basename( id ).slice( 0, -extname( id ).length ) ); // HACK: If `id` isn't a path, the above code yields the empty string. if ( !this.name ) { this.name = makeLegalIdentifier( 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 }); removeSourceMappingURLComments( source, this.magicString ); this.comments = []; this.statements = this.parse( ast ); // all dependencies this.resolvedIds = blank(); // Virtual scopes for the local and exported names. this.locals = bundle.scope.virtual( true ); this.exports = bundle.scope.virtual( false ); const { reference, inScope } = this.exports; this.exports.reference = name => { // If we have it, grab it. if ( inScope.call( this.exports, name ) ) { return reference.call( this.exports, name ); } // ... otherwise search allExportsFrom for ( let i = 0; i < this.allExportsFrom.length; i += 1 ) { const module = this.allExportsFrom[i]; if ( module.exports.inScope( name ) ) { return module.exports.reference( name ); } } // throw new Error( `The name "${name}" is never exported (from ${this.id})!` ); this.exports.define( name, new LateBoundIdPlaceholder( this, name ) ); return reference.call( this.exports, name ); }; this.exports.inScope = name => { if ( inScope.call( this.exports, name ) ) return true; return this.allExportsFrom.some( module => module.exports.inScope( name ) ); }; // Create a unique virtual scope for references to the module. // const unique = bundle.scope.virtual(); // unique.define( this.name, this ); // this.reference = unique.reference( this.name ); // As far as we know, all our exported bindings have been resolved. this.allExportsResolved = true; this.allExportsFrom = []; this.reassignments = []; // TODO: change to false, and detect when it's necessary. this.needsDynamicAccess = false; this.dependencies = this.collectDependencies(); } addExport ( statement ) { const node = statement.node; const source = node.source && node.source.value; // export { name } from './other' if ( source ) { const module = this.getModule( source ); if ( node.type === 'ExportAllDeclaration' ) { // Store `export * from '...'` statements in an array of delegates. // When an unknown import is encountered, we see if one of them can satisfy it. if ( module.isExternal ) { let err = new Error( `Cannot trace 'export *' references through external modules.` ); err.file = this.id; err.loc = getLocation( this.source, node.start ); throw err; } // It seems like we must re-export all exports from another module... this.allExportsResolved = false; if ( !~this.allExportsFrom.indexOf( module ) ) { this.allExportsFrom.push( module ); } } else { node.specifiers.forEach( specifier => { // Bind the export of this module, to the export of the other. this.exports.bind( specifier.exported.name, module.exports.reference( specifier.local.name ) ); }); } } // export default function foo () {} // export default foo; // export default 42; else if ( node.type === 'ExportDefaultDeclaration' ) { const isDeclaration = /Declaration$/.test( node.declaration.type ); const isAnonymous = /(?:Class|Function)Expression$/.test( node.declaration.type ); const identifier = isDeclaration ? node.declaration.id.name : node.declaration.type === 'Identifier' ? node.declaration.name : null; const name = identifier || this.name; // Always define a new `Identifier` for the default export. const id = new Id( this, name, statement ); // Keep the identifier name, if one exists. // We can optimize the newly created default `Identifier` away, // if it is never modified. // in case of `export default foo; foo = somethingElse` assign( id, { isDeclaration, isAnonymous, identifier } ); this.exports.define( 'default', id ); } // 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.bind( exportedName, this.locals.reference( 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.locals.define( name, new Id( this, name, statement ) ); this.exports.bind( name, this.locals.reference( name ) ); } } } addImport ( statement ) { const node = statement.node; const module = this.getModule( node.source.value ); node.specifiers.forEach( specifier => { const isDefault = specifier.type === 'ImportDefaultSpecifier'; const isNamespace = specifier.type === 'ImportNamespaceSpecifier'; const localName = specifier.local.name; if ( this.locals.defines( localName ) ) { const err = new Error( `Duplicated import '${localName}'` ); err.file = this.id; err.loc = getLocation( this.source, specifier.start ); throw err; } if ( isNamespace ) { // If it's a namespace import, we bind the localName to the module itself. module.needsAll = true; module.name = localName; this.locals.bind( localName, module ); } else { const name = isDefault ? 'default' : specifier.imported.name; this.locals.bind( localName, module.exports.reference( name ) ); // For compliance with earlier Rollup versions. // If the module is external, and we access the default. // Rewrite the module name, and the default name to the // `localName` we use for it. if ( module.isExternal && isDefault ) { const id = module.exports.lookup( name ); module.name = id.name = localName; id.name += '__default'; } } }); } analyse () { // discover this module's imports and exports this.statements.forEach( statement => { if ( statement.isImportDeclaration ) this.addImport( statement ); else if ( statement.isExportDeclaration ) this.addExport( statement ); statement.analyse(); // consolidate names that are defined/modified in this module keys( statement.defines ).forEach( name => { this.locals.define( name, new Id( this, name, statement ) ); }); }); // If all exports aren't resolved, but all our delegate modules are... if ( !this.allExportsResolved && this.allExportsFrom.every( module => module.allExportsResolved )) { // .. then all our exports should be as well. this.allExportsResolved = true; // For all modules we export all from, iterate through its exported names. // If we don't already define the binding 'name', // bind the name to the other module's reference. this.allExportsFrom.forEach( module => { module.exports.getNames().forEach( name => { if ( name !== 'default' && !this.exports.defines( name ) ) { this.exports.bind( name, module.exports.reference( name ) ); } }); }); } // discover variables that are reassigned inside function // bodies, so we can keep bindings live, e.g. // // export var count = 0; // export function incr () { count += 1 } let reassigned = blank(); this.statements.forEach( statement => { keys( statement.reassigns ).forEach( name => { reassigned[ name ] = true; }); }); // if names are referenced that are neither defined nor imported // in this module, we assume that they're globals this.statements.forEach( statement => { if ( statement.isReexportDeclaration ) return; // while we're here, mark reassignments statement.scope.varDeclarations.forEach( name => { if ( reassigned[ name ] && !~this.reassignments.indexOf( name ) ) { this.reassignments.push( name ); } }); keys( statement.dependsOn ).forEach( name => { // For each name we depend on that isn't in scope, // add a new global and bind the local name to it. if ( !this.locals.inScope( name ) ) { this.bundle.globals.define( name, { originalName: name, name, mark () {} }); this.locals.bind( name, this.bundle.globals.reference( name ) ); } }); }); // OPTIMIZATION! // If we have a default export and it's value is never modified, // bind to it directly. const def = this.exports.lookup( 'default' ); if ( def && !def.isModified && def.identifier ) { this.exports.bind( 'default', this.locals.reference( def.identifier ) ); } } // Returns the set of imported module ids by going through all import/exports statements. collectDependencies () { const importedModules = blank(); this.statements.forEach( statement => { if ( statement.isImportDeclaration || ( statement.isExportDeclaration && statement.node.source ) ) { importedModules[ statement.node.source.value ] = true; } }); return keys( importedModules ); } consolidateDependencies () { let strongDependencies = blank(); function addDependency ( dependencies, declaration ) { if ( declaration && declaration.module && !declaration.module.isExternal ) { dependencies[ declaration.module.id ] = declaration.module; return true; } } this.statements.forEach( statement => { if ( statement.isImportDeclaration && !statement.node.specifiers.length ) { // include module for its side-effects const module = this.getModule( statement.node.source.value ); if ( !module.isExternal ) strongDependencies[ module.id ] = module; } else if ( statement.isReexportDeclaration ) { if ( statement.node.specifiers ) { statement.node.specifiers.forEach( specifier => { let name = specifier.exported.name; let id = this.exports.lookup( name ); addDependency( strongDependencies, id ); }); } } else { keys( statement.stronglyDependsOn ).forEach( name => { if ( statement.defines[ name ] ) return; addDependency( strongDependencies, this.locals.lookup( name ) ); }); } }); let weakDependencies = blank(); this.statements.forEach( statement => { keys( statement.dependsOn ).forEach( name => { if ( statement.defines[ name ] ) return; addDependency( weakDependencies, this.locals.lookup( name ) ); }); }); // Go through all our local and exported ids and make us depend on // the defining modules as well as this.exports.getIds().concat(this.locals.getIds()).forEach( id => { if ( id.module && !id.module.isExternal ) { weakDependencies[ id.module.id ] = id.module; } if ( !id.modifierStatements ) return; id.modifierStatements.forEach( statement => { const module = statement.module; weakDependencies[ module.id ] = module; }); }); // `Bundle.sort` gets stuck in an infinite loop if a module has // `strongDependencies` to itself. Make sure it doesn't happen. delete strongDependencies[ this.id ]; delete weakDependencies[ this.id ]; return { strongDependencies, weakDependencies }; } getModule ( source ) { return this.bundle.moduleById[ this.resolvedIds[ source ] ]; } // If a module is marked, enforce dynamic access of its properties. mark () { if ( this.needsDynamicAccess ) return; this.needsDynamicAccess = true; this.markAllExports(); } markAllSideEffects () { this.statements.forEach( statement => { statement.markSideEffect(); }); } markAllStatements ( isEntryModule ) { this.statements.forEach( 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 ) { const otherModule = this.getModule( statement.node.source.value ); if ( !otherModule.isExternal ) otherModule.markAllStatements(); } } // skip `export { foo, bar, baz }`... else if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) { // ...but ensure they are defined, if this is the entry module if ( isEntryModule ) statement.mark(); } // include everything else else { // Be sure to mark the default export for the entry module. if ( isEntryModule && statement.node.type === 'ExportDefaultDeclaration' ) { this.exports.lookup( 'default' ).mark(); } statement.mark(); } }); } // Marks all exported identifiers. markAllExports () { this.exports.getIds().forEach( id => id.mark() ); } parse ( ast ) { // The ast can be supplied programmatically (but usually won't be) if ( !ast ) { // Try to extract a list of top-level statements/declarations. If // the parse fails, attach file info and abort try { ast = parse( this.source, { ecmaVersion: 6, sourceType: 'module', onComment: ( block, text, start, end ) => this.comments.push({ block, text, start, end }), preserveParens: true }); } catch ( err ) { err.code = 'PARSE_ERROR'; err.file = this.id; // see above - not necessarily true, but true enough err.message += ` in ${this.id}`; throw err; } } walk( ast, { enter: node => { this.magicString.addSourcemapLocation( node.start ); this.magicString.addSourcemapLocation( node.end ); } }); let statements = []; let lastChar = 0; let commentIndex = 0; 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 ) { // remove the leading var/let/const... UNLESS the previous node // was also a synthetic node, in which case it'll get removed anyway const lastStatement = statements[ statements.length - 1 ]; if ( !lastStatement || !lastStatement.node.isSynthetic ) { this.magicString.remove( node.start, node.declarations[0].start ); } node.declarations.forEach( declarator => { const { start, end } = declarator; const syntheticNode = { type: 'VariableDeclaration', kind: node.kind, start, end, declarations: [ declarator ], isSynthetic: true }; const statement = new Statement( syntheticNode, this, start, end ); statements.push( statement ); }); lastChar = node.end; // TODO account for trailing line comment } else { let comment; do { comment = this.comments[ commentIndex ]; if ( !comment ) break; if ( comment.start > node.start ) break; commentIndex += 1; } while ( comment.end < lastChar ); const start = comment ? Math.min( comment.start, node.start ) : node.start; const end = node.end; // TODO account for trailing line comment const statement = new Statement( node, this, start, end ); statements.push( statement ); lastChar = end; } }); statements.forEach( ( statement, i ) => { const nextStatement = statements[ i + 1 ]; statement.next = nextStatement ? nextStatement.start : statement.end; }); return statements; } render ( toExport, direct ) { let magicString = this.magicString.clone(); 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; } } // split up/remove var declarations as necessary if ( statement.node.isSynthetic ) { // insert `var/let/const` if necessary if ( !toExport[ statement.node.declarations[0].id.name ] ) { magicString.insert( statement.start, `${statement.node.kind} ` ); } magicString.overwrite( statement.end, statement.next, ';\n' ); // TODO account for trailing newlines } let replacements = blank(); let bundleExports = blank(); // Indirect identifier access. if ( !direct ) { keys( statement.dependsOn ) .forEach( name => { const id = this.locals.lookup( name ); // We shouldn't create a replacement for `id` if // 1. `id` is a Global, in which case it has no module property // 2. `id.module` isn't external, which means we have direct access // 3. `id` is its own module, in the case of namespace imports if ( id.module && id.module.isExternal && id.module !== id ) { replacements[ name ] = id.originalName === 'default' ? // default names are always directly accessed id.name : // other names are indirectly accessed `${id.module.name}.${id.originalName}`; } }); } keys( statement.dependsOn ) .concat( keys( statement.defines ) ) .forEach( name => { const bundleName = this.locals.lookup( name ).name; if ( toExport[ bundleName ] ) { bundleExports[ name ] = replacements[ name ] = toExport[ bundleName ]; } else if ( bundleName !== name && !replacements[ name ] ) { // TODO weird structure replacements[ name ] = bundleName; } }); 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 ); } else if ( statement.node.type === 'ExportAllDeclaration' ) { // TODO: remove once `export * from 'external'` is supported. magicString.remove( statement.start, statement.next ); } // 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 def = this.exports.lookup( 'default' ); // FIXME: dunno what to do here yet. if ( statement.node.declaration.type === 'Identifier' && def.name === ( replacements[ statement.node.declaration.name ] || statement.node.declaration.name ) ) { magicString.remove( statement.start, statement.next ); return; } // prevent `var undefined = sideEffectyDefault(foo)` if ( !def.isUsed ) { magicString.remove( statement.start, statement.node.declaration.start ); 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 ${def.name}` ); } else { magicString.overwrite( statement.node.start, statement.node.declaration.start, `var ${def.name} = ` ); } } else { throw new Error( 'Unhandled export' ); } } }); return magicString.trim(); } }