Browse Source

start moving render logic into modules

contingency-plan
Rich-Harris 10 years ago
parent
commit
9435773b1c
  1. 157
      src/Bundle.js
  2. 177
      src/Module.js
  3. 18
      src/Statement.js
  4. 4
      src/rollup.js

157
src/Bundle.js

@ -13,15 +13,6 @@ import getExportMode from './utils/getExportMode';
import getIndentString from './utils/getIndentString'; import getIndentString from './utils/getIndentString';
import { unixizePath } from './utils/normalizePlatform.js'; import { unixizePath } from './utils/normalizePlatform.js';
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 Bundle { export default class Bundle {
constructor ( options ) { constructor ( options ) {
this.entry = options.entry; this.entry = options.entry;
@ -90,7 +81,7 @@ export default class Bundle {
return this.markAllModifierStatements(); return this.markAllModifierStatements();
}) })
.then( () => { .then( () => {
this.statements = this.sort(); this.orderedModules = this.sort();
}); });
} }
@ -115,34 +106,35 @@ export default class Bundle {
}); });
// Discover conflicts (i.e. two statements in separate modules both define `foo`) // Discover conflicts (i.e. two statements in separate modules both define `foo`)
this.statements.forEach( statement => { this.orderedModules.forEach( module => {
const module = statement.module; module.statements.forEach( statement => {
const names = keys( statement.defines ); const names = keys( statement.defines );
// with default exports that are expressions (`export default 42`), // with default exports that are expressions (`export default 42`),
// we need to ensure that the name chosen for the expression does // we need to ensure that the name chosen for the expression does
// not conflict // not conflict
if ( statement.node.type === 'ExportDefaultDeclaration' ) { if ( statement.node.type === 'ExportDefaultDeclaration' ) {
const name = module.getCanonicalName( 'default', es6 ); const name = module.getCanonicalName( 'default', es6 );
const isProxy = statement.node.declaration && statement.node.declaration.type === 'Identifier'; const isProxy = statement.node.declaration && statement.node.declaration.type === 'Identifier';
const shouldDeconflict = !isProxy || ( module.getCanonicalName( statement.node.declaration.name, es6 ) !== name ); const shouldDeconflict = !isProxy || ( module.getCanonicalName( statement.node.declaration.name, es6 ) !== name );
if ( shouldDeconflict && !~names.indexOf( name ) ) { if ( shouldDeconflict && !~names.indexOf( name ) ) {
names.push( name ); names.push( name );
}
} }
}
names.forEach( name => { names.forEach( name => {
if ( definers[ name ] ) { if ( definers[ name ] ) {
conflicts[ name ] = true; conflicts[ name ] = true;
} else { } else {
definers[ name ] = []; definers[ name ] = [];
} }
// TODO in good js, there shouldn't be duplicate definitions // TODO in good js, there shouldn't be duplicate definitions
// per module... but some people write bad js // per module... but some people write bad js
definers[ name ].push( module ); definers[ name ].push( module );
});
}); });
}); });
@ -459,6 +451,97 @@ export default class Bundle {
}); });
} }
render ( options = {} ) {
const format = options.format || 'es6';
this.deconflict( format === 'es6' );
// If we have named exports from the bundle, and those exports
// are assigned to *within* the bundle, we may need to rewrite e.g.
//
// export let count = 0;
// export function incr () { count++ }
//
// might become...
//
// exports.count = 0;
// function incr () {
// exports.count += 1;
// }
// exports.incr = incr;
//
// This doesn't apply if the bundle is exported as ES6!
let allBundleExports = blank();
if ( format !== 'es6' ) {
keys( this.entryModule.exports ).forEach( key => {
const exportDeclaration = this.entryModule.exports[ key ];
const originalDeclaration = this.entryModule.findDeclaration( exportDeclaration.localName );
if ( originalDeclaration && originalDeclaration.type === 'VariableDeclaration' ) {
const canonicalName = this.entryModule.getCanonicalName( exportDeclaration.localName, false );
allBundleExports[ canonicalName ] = `exports.${key}`;
this.varExports[ key ] = true;
}
});
}
// since we're rewriting variable exports, we want to
// ensure we don't try and export them again at the bottom
this.toExport = keys( this.entryModule.exports )
.filter( key => !this.varExports[ key ] );
let magicString = new MagicString.Bundle({ separator: '\n\n' });
this.orderedModules.forEach( module => {
magicString.addSource( module.render( allBundleExports, format ) );
});
// prepend bundle with internal namespaces
const indentString = magicString.getIndentString();
const namespaceBlock = this.internalNamespaceModules.map( module => {
const exportKeys = keys( module.exports );
return `var ${module.getCanonicalName('*', format === 'es6')} = {\n` +
exportKeys.map( key => `${indentString}get ${key} () { return ${module.getCanonicalName(key, format === 'es6')}; }` ).join( ',\n' ) +
`\n};\n\n`;
}).join( '' );
magicString.prepend( namespaceBlock );
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(), {
// Determine export mode - 'default', 'named', 'none'
exportMode: getExportMode( this, options.exports ),
// Determine indentation
indentString: getIndentString( magicString, options )
}, options );
const code = magicString.toString();
let map = null;
if ( options.sourceMap ) {
const file = options.sourceMapFile || options.dest;
map = magicString.generateMap({
includeContent: true,
file
// TODO
});
map.sources = map.sources.map( unixizePath );
}
return { code, map };
}
sort () { sort () {
let seen = {}; let seen = {};
let ordered = []; let ordered = [];
@ -540,14 +623,6 @@ export default class Bundle {
}); });
} }
let statements = []; return ordered;
ordered.forEach( module => {
module.statements.forEach( statement => {
if ( statement.isIncluded ) statements.push( statement );
});
});
return statements;
} }
} }

177
src/Module.js

@ -21,6 +21,15 @@ function deconflict ( name, names ) {
return 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 { export default class Module {
constructor ({ id, source, bundle }) { constructor ({ id, source, bundle }) {
this.source = source; this.source = source;
@ -531,36 +540,41 @@ export default class Module {
let statements = []; let statements = [];
ast.body.map( node => { ast.body.forEach( node => {
// special case - top-level var declarations with multiple declarators // special case - top-level var declarations with multiple declarators
// should be split up. Otherwise, we may end up including code we // should be split up. Otherwise, we may end up including code we
// don't need, just because an unwanted declarator is included // don't need, just because an unwanted declarator is included
if ( node.type === 'VariableDeclaration' && node.declarations.length > 1 ) { if ( node.type === 'VariableDeclaration' && node.declarations.length > 1 ) {
node.declarations.forEach( declarator => { throw new Error( 'TODO' );
const magicString = this.magicString.snip( declarator.start, declarator.end ).trim(); // node.declarations.forEach( declarator => {
magicString.prepend( `${node.kind} ` ).append( ';' ); // const magicString = this.magicString.snip( declarator.start, declarator.end ).trim();
// magicString.prepend( `${node.kind} ` ).append( ';' );
const syntheticNode = { //
type: 'VariableDeclaration', // const syntheticNode = {
kind: node.kind, // type: 'VariableDeclaration',
start: node.start, // kind: node.kind,
end: node.end, // start: node.start,
declarations: [ declarator ] // end: node.end,
}; // declarations: [ declarator ]
// };
const statement = new Statement( syntheticNode, magicString, this, statements.length ); //
statements.push( statement ); // const statement = new Statement( syntheticNode, magicString, this, statements.length );
}); // statements.push( statement );
// });
} }
else { else {
const magicString = this.magicString.snip( node.start, node.end ).trim(); const statement = new Statement( node, this, node.start, node.end ); // TODO should be comment start, comment end
const statement = new Statement( node, magicString, this, statements.length );
statements.push( statement ); statements.push( statement );
} }
}); });
statements.forEach( ( statement, i ) => {
const nextStatement = statements[ i + 1 ];
statement.next = nextStatement ? nextStatement.start : statement.end;
});
return statements; return statements;
} }
@ -569,6 +583,133 @@ export default class Module {
this.canonicalNames[ name ] = this.canonicalNames[ name + '-es6' ] = replacement; 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 ) { suggestName ( defaultOrBatch, suggestion ) {
// deconflict anonymous default exports with this module's definitions // deconflict anonymous default exports with this module's definitions
const shouldDeconflict = this.exports.default && this.exports.default.isAnonymous; const shouldDeconflict = this.exports.default && this.exports.default.isAnonymous;

18
src/Statement.js

@ -9,12 +9,12 @@ function isIife ( node, parent ) {
} }
export default class Statement { export default class Statement {
constructor ( node, magicString, module, index ) { constructor ( node, module, start, end ) {
this.node = node; this.node = node;
this.module = module; this.module = module;
this.magicString = magicString; this.start = start;
this.index = index; this.end = end;
this.id = module.id + '#' + index; this.next = null; // filled in later
this.scope = new Scope(); this.scope = new Scope();
this.defines = blank(); this.defines = blank();
@ -37,17 +37,12 @@ export default class Statement {
analyse () { analyse () {
if ( this.isImportDeclaration ) return; // nothing to analyse if ( this.isImportDeclaration ) return; // nothing to analyse
const statement = this; // TODO use arrow functions instead
const magicString = this.magicString;
let scope = this.scope; let scope = this.scope;
walk( this.node, { walk( this.node, {
enter ( node, parent ) { enter ( node, parent ) {
let newScope; let newScope;
magicString.addSourcemapLocation( node.start );
switch ( node.type ) { switch ( node.type ) {
case 'FunctionExpression': case 'FunctionExpression':
case 'FunctionDeclaration': case 'FunctionDeclaration':
@ -146,7 +141,7 @@ export default class Statement {
} }
keys( scope.declarations ).forEach( name => { keys( scope.declarations ).forEach( name => {
statement.defines[ name ] = true; this.defines[ name ] = true;
}); });
} }
@ -247,8 +242,7 @@ export default class Statement {
}); });
} }
replaceIdentifiers ( names, bundleExports ) { replaceIdentifiers ( magicString, names, bundleExports ) {
const magicString = this.magicString.clone();
const replacementStack = [ names ]; const replacementStack = [ names ];
const nameList = keys( names ); const nameList = keys( names );

4
src/rollup.js

@ -14,14 +14,14 @@ export function rollup ( options ) {
return bundle.build().then( () => { return bundle.build().then( () => {
return { return {
generate: options => bundle.generate( options ), generate: options => bundle.render( options ),
write: options => { write: options => {
if ( !options || !options.dest ) { if ( !options || !options.dest ) {
throw new Error( 'You must supply options.dest to bundle.write' ); throw new Error( 'You must supply options.dest to bundle.write' );
} }
const dest = options.dest; const dest = options.dest;
let { code, map } = bundle.generate( options ); let { code, map } = bundle.render( options );
let promises = []; let promises = [];

Loading…
Cancel
Save