mirror of https://github.com/lukechilds/rollup.git
399 lines
11 KiB
399 lines
11 KiB
import { timeStart, timeEnd } from './utils/flushTime.js';
|
|
import { parse } from 'acorn/src/index.js';
|
|
import MagicString from 'magic-string';
|
|
import { assign, blank, deepClone, keys } from './utils/object.js';
|
|
import { basename, extname } from './utils/path.js';
|
|
import getLocation from './utils/getLocation.js';
|
|
import makeLegalIdentifier from './utils/makeLegalIdentifier.js';
|
|
import SOURCEMAPPING_URL from './utils/sourceMappingURL.js';
|
|
import { SyntheticNamespaceDeclaration } from './Declaration.js';
|
|
import extractNames from './ast/utils/extractNames.js';
|
|
import enhance from './ast/enhance.js';
|
|
import ModuleScope from './ast/scopes/ModuleScope.js';
|
|
|
|
function tryParse ( code, comments, acornOptions, id ) {
|
|
try {
|
|
return parse( code, assign({
|
|
ecmaVersion: 7,
|
|
sourceType: 'module',
|
|
onComment: ( block, text, start, end ) => comments.push({ block, text, start, end }),
|
|
preserveParens: true
|
|
}, acornOptions ));
|
|
} catch ( err ) {
|
|
err.code = 'PARSE_ERROR';
|
|
err.file = id; // see above - not necessarily true, but true enough
|
|
err.message += ` in ${id}`;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export default class Module {
|
|
constructor ({ id, code, originalCode, originalSourceMap, ast, sourceMapChain, resolvedIds, bundle }) {
|
|
this.code = code;
|
|
this.originalCode = originalCode;
|
|
this.originalSourceMap = originalSourceMap;
|
|
this.sourceMapChain = sourceMapChain;
|
|
|
|
this.comments = [];
|
|
|
|
timeStart( 'ast' );
|
|
|
|
this.ast = ast || tryParse( code, this.comments, bundle.acornOptions, id ); // TODO what happens to comments if AST is provided?
|
|
this.astClone = deepClone( this.ast );
|
|
|
|
timeEnd( 'ast' );
|
|
|
|
this.bundle = bundle;
|
|
this.id = id;
|
|
this.excludeFromSourcemap = /\0/.test( id );
|
|
this.context = bundle.getModuleContext( id );
|
|
|
|
// all dependencies
|
|
this.sources = [];
|
|
this.dependencies = [];
|
|
this.resolvedIds = resolvedIds || blank();
|
|
|
|
// imports and exports, indexed by local name
|
|
this.imports = blank();
|
|
this.exports = blank();
|
|
this.exportsAll = blank();
|
|
this.reexports = blank();
|
|
|
|
this.exportAllSources = [];
|
|
this.exportAllModules = null;
|
|
|
|
// 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( code, {
|
|
filename: this.excludeFromSourcemap ? null : id, // don't include plugin helpers in sourcemap
|
|
indentExclusionRanges: []
|
|
});
|
|
|
|
// remove existing sourceMappingURL comments
|
|
const pattern = new RegExp( `\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'g' );
|
|
let match;
|
|
while ( match = pattern.exec( code ) ) {
|
|
this.magicString.remove( match.index, match.index + match[0].length );
|
|
}
|
|
|
|
this.declarations = blank();
|
|
this.type = 'Module'; // TODO only necessary so that Scope knows this should be treated as a function scope... messy
|
|
this.scope = new ModuleScope( this );
|
|
|
|
timeStart( 'analyse' );
|
|
|
|
this.analyse();
|
|
|
|
timeEnd( 'analyse' );
|
|
|
|
this.strongDependencies = [];
|
|
}
|
|
|
|
addExport ( node ) {
|
|
const source = node.source && node.source.value;
|
|
|
|
// export { name } from './other.js'
|
|
if ( source ) {
|
|
if ( !~this.sources.indexOf( source ) ) this.sources.push( 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.
|
|
this.exportAllSources.push( source );
|
|
}
|
|
|
|
else {
|
|
node.specifiers.forEach( specifier => {
|
|
const name = specifier.exported.name;
|
|
|
|
if ( this.exports[ name ] || this.reexports[ name ] ) {
|
|
throw new Error( `A module cannot have multiple exports with the same name ('${name}')` );
|
|
}
|
|
|
|
this.reexports[ name ] = {
|
|
start: specifier.start,
|
|
source,
|
|
localName: specifier.local.name,
|
|
module: null // filled in later
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
// export default function foo () {}
|
|
// export default foo;
|
|
// export default 42;
|
|
else if ( node.type === 'ExportDefaultDeclaration' ) {
|
|
const identifier = ( node.declaration.id && node.declaration.id.name ) || node.declaration.name;
|
|
|
|
if ( this.exports.default ) {
|
|
// TODO indicate location
|
|
throw new Error( 'A module can only have one default export' );
|
|
}
|
|
|
|
this.exports.default = {
|
|
localName: 'default',
|
|
identifier
|
|
};
|
|
|
|
// create a synthetic declaration
|
|
//this.declarations.default = new SyntheticDefaultDeclaration( node, identifier || this.basename() );
|
|
}
|
|
|
|
// export var { foo, bar } = ...
|
|
// export var foo = 42;
|
|
// export var a = 1, b = 2, c = 3;
|
|
// export function foo () {}
|
|
else if ( node.declaration ) {
|
|
const declaration = node.declaration;
|
|
|
|
if ( declaration.type === 'VariableDeclaration' ) {
|
|
declaration.declarations.forEach( decl => {
|
|
extractNames( decl.id ).forEach( localName => {
|
|
this.exports[ localName ] = { localName };
|
|
});
|
|
});
|
|
} else {
|
|
// export function foo () {}
|
|
const localName = declaration.id.name;
|
|
this.exports[ localName ] = { localName };
|
|
}
|
|
}
|
|
|
|
// export { foo, bar, baz }
|
|
else {
|
|
if ( node.specifiers.length ) {
|
|
node.specifiers.forEach( specifier => {
|
|
const localName = specifier.local.name;
|
|
const exportedName = specifier.exported.name;
|
|
|
|
if ( this.exports[ exportedName ] || this.reexports[ exportedName ] ) {
|
|
throw new Error( `A module cannot have multiple exports with the same name ('${exportedName}')` );
|
|
}
|
|
|
|
// `export { default as foo }` – special case. We want importers
|
|
// to use the UnboundDefaultExport proxy, not the original declaration
|
|
if ( exportedName === 'default' ) {
|
|
this.exports[ exportedName ] = { localName: 'default' };
|
|
} else {
|
|
this.exports[ exportedName ] = { localName };
|
|
}
|
|
});
|
|
} else {
|
|
this.bundle.onwarn( `Module ${this.id} has an empty export declaration` );
|
|
}
|
|
}
|
|
}
|
|
|
|
addImport ( node ) {
|
|
const source = node.source.value;
|
|
|
|
if ( !~this.sources.indexOf( source ) ) this.sources.push( source );
|
|
|
|
node.specifiers.forEach( specifier => {
|
|
const localName = specifier.local.name;
|
|
|
|
if ( this.imports[ localName ] ) {
|
|
const err = new Error( `Duplicated import '${localName}'` );
|
|
err.file = this.id;
|
|
err.loc = getLocation( this.code, specifier.start );
|
|
throw err;
|
|
}
|
|
|
|
const isDefault = specifier.type === 'ImportDefaultSpecifier';
|
|
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
|
|
|
|
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
|
|
this.imports[ localName ] = { source, name, module: null };
|
|
});
|
|
}
|
|
|
|
analyse () {
|
|
enhance( this.ast, this, this.comments );
|
|
|
|
// discover this module's imports and exports
|
|
let lastNode;
|
|
|
|
for ( const node of this.ast.body ) {
|
|
if ( node.isImportDeclaration ) {
|
|
this.addImport( node );
|
|
} else if ( node.isExportDeclaration ) {
|
|
this.addExport( node );
|
|
}
|
|
|
|
if ( lastNode ) lastNode.next = node.leadingCommentStart || node.start;
|
|
lastNode = node;
|
|
}
|
|
}
|
|
|
|
basename () {
|
|
const base = basename( this.id );
|
|
const ext = extname( this.id );
|
|
|
|
return makeLegalIdentifier( ext ? base.slice( 0, -ext.length ) : base );
|
|
}
|
|
|
|
bindImportSpecifiers () {
|
|
[ this.imports, this.reexports ].forEach( specifiers => {
|
|
keys( specifiers ).forEach( name => {
|
|
const specifier = specifiers[ name ];
|
|
|
|
const id = this.resolvedIds[ specifier.source ];
|
|
specifier.module = this.bundle.moduleById.get( id );
|
|
});
|
|
});
|
|
|
|
this.exportAllModules = this.exportAllSources.map( source => {
|
|
const id = this.resolvedIds[ source ];
|
|
return this.bundle.moduleById.get( id );
|
|
});
|
|
|
|
this.sources.forEach( source => {
|
|
const id = this.resolvedIds[ source ];
|
|
const module = this.bundle.moduleById.get( id );
|
|
|
|
if ( !module.isExternal ) this.dependencies.push( module );
|
|
});
|
|
}
|
|
|
|
bindReferences () {
|
|
for ( const node of this.ast.body ) {
|
|
node.bind( this.scope );
|
|
}
|
|
|
|
// if ( this.declarations.default ) {
|
|
// if ( this.exports.default.identifier ) {
|
|
// const declaration = this.trace( this.exports.default.identifier );
|
|
// if ( declaration ) this.declarations.default.bind( declaration );
|
|
// }
|
|
// }
|
|
}
|
|
|
|
findParent () {
|
|
// TODO what does it mean if we're here?
|
|
return null;
|
|
}
|
|
|
|
findScope () {
|
|
return this.scope;
|
|
}
|
|
|
|
getExports () {
|
|
const exports = blank();
|
|
|
|
keys( this.exports ).forEach( name => {
|
|
exports[ name ] = true;
|
|
});
|
|
|
|
keys( this.reexports ).forEach( name => {
|
|
exports[ name ] = true;
|
|
});
|
|
|
|
this.exportAllModules.forEach( module => {
|
|
if ( module.isExternal ) return; // TODO
|
|
|
|
module.getExports().forEach( name => {
|
|
if ( name !== 'default' ) exports[ name ] = true;
|
|
});
|
|
});
|
|
|
|
return keys( exports );
|
|
}
|
|
|
|
namespace () {
|
|
if ( !this.declarations['*'] ) {
|
|
this.declarations['*'] = new SyntheticNamespaceDeclaration( this );
|
|
}
|
|
|
|
return this.declarations['*'];
|
|
}
|
|
|
|
render ( es ) {
|
|
const magicString = this.magicString.clone();
|
|
|
|
for ( const node of this.ast.body ) {
|
|
node.render( magicString, es );
|
|
}
|
|
|
|
if ( this.namespace().needsNamespaceBlock ) {
|
|
magicString.append( '\n\n' + this.namespace().renderBlock( es, '\t' ) ); // TODO use correct indentation
|
|
}
|
|
|
|
return magicString.trim();
|
|
}
|
|
|
|
run () {
|
|
for ( const node of this.ast.body ) {
|
|
if ( node.hasEffects( this.scope ) ) {
|
|
node.run( this.scope );
|
|
}
|
|
}
|
|
}
|
|
|
|
toJSON () {
|
|
return {
|
|
id: this.id,
|
|
dependencies: this.dependencies.map( module => module.id ),
|
|
code: this.code,
|
|
originalCode: this.originalCode,
|
|
ast: this.astClone,
|
|
sourceMapChain: this.sourceMapChain,
|
|
resolvedIds: this.resolvedIds
|
|
};
|
|
}
|
|
|
|
trace ( name ) {
|
|
// TODO this is slightly circular
|
|
if ( name in this.scope.declarations ) {
|
|
return this.scope.declarations[ name ];
|
|
}
|
|
|
|
if ( name in this.imports ) {
|
|
const importDeclaration = this.imports[ name ];
|
|
const otherModule = importDeclaration.module;
|
|
|
|
if ( importDeclaration.name === '*' && !otherModule.isExternal ) {
|
|
return otherModule.namespace();
|
|
}
|
|
|
|
const declaration = otherModule.traceExport( importDeclaration.name );
|
|
|
|
if ( !declaration ) throw new Error( `Module ${otherModule.id} does not export ${importDeclaration.name} (imported by ${this.id})` );
|
|
return declaration;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
traceExport ( name ) {
|
|
// export { foo } from './other.js'
|
|
const reexportDeclaration = this.reexports[ name ];
|
|
if ( reexportDeclaration ) {
|
|
const declaration = reexportDeclaration.module.traceExport( reexportDeclaration.localName );
|
|
|
|
if ( !declaration ) {
|
|
const err = new Error( `'${reexportDeclaration.localName}' is not exported by '${reexportDeclaration.module.id}' (imported by '${this.id}')` );
|
|
err.file = this.id;
|
|
err.loc = getLocation( this.code, reexportDeclaration.start );
|
|
throw err;
|
|
}
|
|
|
|
return declaration;
|
|
}
|
|
|
|
const exportDeclaration = this.exports[ name ];
|
|
if ( exportDeclaration ) {
|
|
const name = exportDeclaration.localName;
|
|
const declaration = this.trace( name );
|
|
|
|
return declaration || this.bundle.scope.findDeclaration( name );
|
|
}
|
|
|
|
for ( let i = 0; i < this.exportAllModules.length; i += 1 ) {
|
|
const module = this.exportAllModules[i];
|
|
const declaration = module.traceExport( name );
|
|
|
|
if ( declaration ) return declaration;
|
|
}
|
|
}
|
|
}
|
|
|