Browse Source

generate UMD output, handle cycles, various fixes

contingency-plan
Rich-Harris 10 years ago
parent
commit
9371a6f709
  1. 4
      package.json
  2. 21
      src/Bundle/index.js
  3. 147
      src/Module/index.js
  4. 45
      src/ast/analyse.js
  5. 3
      src/finalisers/amd.js
  6. 3
      src/finalisers/cjs.js
  7. 3
      src/finalisers/es6.js
  8. 6
      src/finalisers/index.js
  9. 24
      src/finalisers/umd.js
  10. 7
      src/rollup.js
  11. 3
      test/samples/import-default-function/_config.js
  12. 3
      test/samples/try-catch-scoping/_config.js
  13. 7
      test/samples/try-catch-scoping/foo.js
  14. 2
      test/samples/try-catch-scoping/main.js

4
package.json

@ -2,8 +2,8 @@
"name": "rollup", "name": "rollup",
"version": "0.1.0", "version": "0.1.0",
"description": "Next-generation ES6 module bundler", "description": "Next-generation ES6 module bundler",
"main": "dist/index.js", "main": "dist/rollup.js",
"jsnext:main": "src/index.js", "jsnext:main": "src/rollup.js",
"scripts": { "scripts": {
"test": "mocha", "test": "mocha",
"pretest": "npm run build", "pretest": "npm run build",

21
src/Bundle/index.js

@ -4,6 +4,7 @@ import MagicString from 'magic-string';
import { hasOwnProp } from '../utils/object'; import { hasOwnProp } from '../utils/object';
import { sequence } from '../utils/promise'; import { sequence } from '../utils/promise';
import Module from '../Module/index'; import Module from '../Module/index';
import finalisers from '../finalisers/index';
export default class Bundle { export default class Bundle {
constructor ( options ) { constructor ( options ) {
@ -20,6 +21,8 @@ export default class Bundle {
// this will store per-module names, and enable deconflicting // this will store per-module names, and enable deconflicting
this.names = {}; this.names = {};
this.usedNames = {}; this.usedNames = {};
this.externalModules = [];
} }
collect () { collect () {
@ -71,16 +74,24 @@ export default class Bundle {
}); });
} }
generate () { generate ( options = {} ) {
const bundle = new MagicString.Bundle(); let magicString = new MagicString.Bundle();
this.body.forEach( statement => { this.body.forEach( statement => {
bundle.addSource( statement._source ); magicString.addSource( statement._source );
}); });
const finalise = finalisers[ options.format || 'es6' ];
if ( !finalise ) {
throw new Error( `You must specify an output type - valid options are ${Object.keys( finalisers ).join( ', ' )}` );
}
magicString = finalise( this, magicString, options );
return { return {
code: bundle.toString(), code: magicString.toString(),
map: null // TODO use bundle.generateMap() map: null // TODO use magicString.generateMap()
}; };
// try { // try {

147
src/Module/index.js

@ -1,14 +1,17 @@
import { dirname, relative, resolve } from 'path'; import { dirname, relative, resolve } from 'path';
import { Promise } from 'sander';
import { parse } from 'acorn'; import { parse } from 'acorn';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import analyse from '../ast/analyse'; import analyse from '../ast/analyse';
import { hasOwnProp } from '../utils/object'; import { hasOwnProp } from '../utils/object';
import { sequence } from '../utils/promise'; import { sequence } from '../utils/promise';
const emptyArrayPromise = Promise.resolve([]);
export default class Module { export default class Module {
constructor ({ path, code, bundle }) { constructor ({ path, code, bundle }) {
this.path = path; this.path = path;
this.relativePath = relative( bundle.base, path ); this.relativePath = relative( bundle.base, path ).slice( 0, -3 ); // remove .js
this.code = new MagicString( code ); this.code = new MagicString( code );
this.bundle = bundle; this.bundle = bundle;
@ -17,9 +20,12 @@ export default class Module {
sourceType: 'module' sourceType: 'module'
}); });
console.log( '\nanalysing %s\n========', path );
analyse( this.ast, this.code ); analyse( this.ast, this.code );
console.log( '========\n\n' );
this.definitions = {}; this.definitions = {};
this.definitionPromises = {};
this.modifications = {}; this.modifications = {};
this.ast.body.forEach( statement => { this.ast.body.forEach( statement => {
@ -52,20 +58,14 @@ export default class Module {
} }
else if ( node.type === 'ExportDefaultDeclaration' ) { else if ( node.type === 'ExportDefaultDeclaration' ) {
//const isDeclaration = /Declaration$/.test( node) const isDeclaration = /Declaration$/.test( node.declaration.type );
this.exports.default = { this.exports.default = {
node, node,
localName: 'default', localName: 'default',
isDeclaration: false, name: isDeclaration ? node.declaration.id.name : null,
//expression: node.declaration isDeclaration
}; };
// special case - need to transfer top-level node tracking info to expression
// TODO this is fugly, refactor it
// if ( this.exports.default.expression ) {
// this.exports.default.expression._dependsOn = node._dependsOn;
// this.exports.default.expression._source = node._source;
// }
} }
else if ( node.type === 'ExportNamedDeclaration' ) { else if ( node.type === 'ExportNamedDeclaration' ) {
@ -103,76 +103,89 @@ export default class Module {
}); });
} }
define ( name, importer ) { define ( name ) {
let statement; // shortcut cycles. TODO this won't work everywhere...
if ( hasOwnProp.call( this.definitionPromises, name ) ) {
// The definition for this name is in a different module return emptyArrayPromise;
if ( hasOwnProp.call( this.imports, name ) ) { }
const importDeclaration = this.imports[ name ];
const path = resolve( dirname( this.path ), importDeclaration.source ) + '.js';
return this.bundle.fetchModule( path ) if ( !hasOwnProp.call( this.definitionPromises, name ) ) {
.then( module => { let promise;
const exportDeclaration = module.exports[ importDeclaration.name ];
if ( !exportDeclaration ) { // The definition for this name is in a different module
throw new Error( `Module ${module.path} does not export ${importDeclaration.name} (imported by ${this.path})` ); if ( hasOwnProp.call( this.imports, name ) ) {
} const importDeclaration = this.imports[ name ];
const path = resolve( dirname( this.path ), importDeclaration.source ) + '.js';
// we 'suggest' that the bundle use our local name for this import promise = this.bundle.fetchModule( path )
// throughout the bundle. If that causes a conflict, we'll end up .then( module => {
// with something slightly different const exportDeclaration = module.exports[ importDeclaration.name ];
this.bundle.suggestName( module, exportDeclaration.localName, importDeclaration.localName );
if ( !exportDeclaration ) {
throw new Error( `Module ${module.path} does not export ${importDeclaration.name} (imported by ${this.path})` );
}
return module.define( exportDeclaration.localName, this ); // we 'suggest' that the bundle use our local name for this import
}); // throughout the bundle. If that causes a conflict, we'll end up
} // with something slightly different
this.bundle.suggestName( module, exportDeclaration.localName, importDeclaration.localName );
// The definition is in this module; it's the default export return module.define( exportDeclaration.localName );
else if ( name === 'default' ) { });
const defaultExport = this.exports.default; }
// We have something like `export default foo` - so we just start again, // The definition is in this module
// searching for `foo` instead of default else if ( name === 'default' && this.exports.default.isDeclaration ) {
if ( defaultExport.isDeclaration ) { // We have something like `export default foo` - so we just start again,
return this.define( defaultExport.name, this ); // searching for `foo` instead of default
promise = this.define( this.exports.default.name );
} }
// Otherwise, we have an expression, e.g. `export default 42`. We have else {
// to assign that expression to a variable let statement;
const name = this.bundle.getName( this, 'default' );
statement = defaultExport.node; if ( name === 'default' ) {
statement._source.overwrite( statement.start, statement.declaration.start, `var ${name} = ` ) // We have an expression, e.g. `export default 42`. We have
} // to assign that expression to a variable
const name = this.bundle.getName( this, 'default' );
else { statement = this.exports.default.node;
statement = this.definitions[ name ];
if ( /^Export/.test( statement.type ) ) { if ( !statement._imported ) {
statement._source.remove( statement.start, statement.declaration.start ); statement._source.overwrite( statement.start, statement.declaration.start, `var ${name} = ` )
} }
} }
else {
statement = this.definitions[ name ];
if ( statement ) { if ( statement && /^Export/.test( statement.type ) ) {
const nodes = []; statement._source.remove( statement.start, statement.declaration.start );
}
return sequence( Object.keys( statement._dependsOn ), name => { }
return this.define( name, this );
}) if ( statement && !statement._imported ) {
.then( definitions => { const nodes = [];
definitions.forEach( definition => nodes.push.apply( nodes, definition ) );
}) promise = sequence( Object.keys( statement._dependsOn ), name => {
.then( () => { return this.define( name );
nodes.push( statement ); })
}) .then( definitions => {
.then( () => { definitions.forEach( definition => nodes.push.apply( nodes, definition ) );
return nodes; })
}); .then( () => {
} else { statement._imported = true;
throw new Error( `Could not define ${name}` ); nodes.push( statement );
})
.then( () => {
return nodes;
});
}
}
this.definitionPromises[ name ] = promise || emptyArrayPromise;
} }
return this.definitionPromises[ name ];
} }
} }

45
src/ast/analyse.js

@ -41,6 +41,7 @@ export default function analyse ( ast, code ) {
statement._defines = {}; statement._defines = {};
statement._modifies = {}; statement._modifies = {};
statement._dependsOn = {}; statement._dependsOn = {};
statement._imported = false;
// store the actual code, for easy regeneration // store the actual code, for easy regeneration
statement._source = code.snip( previous, statement.end ); statement._source = code.snip( previous, statement.end );
@ -50,33 +51,42 @@ export default function analyse ( ast, code ) {
walk( statement, { walk( statement, {
enter ( node, parent ) { enter ( node, parent ) {
let newScope;
switch ( node.type ) { switch ( node.type ) {
case 'FunctionExpression': case 'FunctionExpression':
case 'FunctionDeclaration': case 'FunctionDeclaration':
case 'ArrowFunctionExpression': case 'ArrowFunctionExpression':
if ( node.id ) { let names = node.params.map( getName );
if ( node.type === 'FunctionDeclaration' ) {
addToScope( node ); addToScope( node );
} else if ( node.type === 'FunctionExpression' && node.id ) {
names.push( node.id.name );
} }
let names = node.params.map( getName ); newScope = new Scope({
scope = new Scope({
parent: scope, parent: scope,
params: names, // TODO rest params? params: names, // TODO rest params?
block: false block: false
}); });
Object.defineProperty( node, '_scope', { value: scope });
break; break;
case 'BlockStatement': case 'BlockStatement':
scope = new Scope({ newScope = new Scope({
parent: scope, parent: scope,
block: true block: true
}); });
Object.defineProperty( node, '_scope', { value: scope }); break;
case 'CatchClause':
newScope = new Scope({
parent: scope,
params: [ node.param.name ],
block: true
});
break; break;
@ -89,19 +99,19 @@ export default function analyse ( ast, code ) {
addToScope( node ); addToScope( node );
break; break;
} }
if ( newScope ) {
Object.defineProperty( node, '_scope', { value: newScope });
scope = newScope;
}
}, },
leave ( node ) { leave ( node ) {
if ( node === currentTopLevelStatement ) { if ( node === currentTopLevelStatement ) {
currentTopLevelStatement = null; currentTopLevelStatement = null;
} }
switch ( node.type ) { if ( node._scope ) {
case 'FunctionExpression': scope = scope.parent;
case 'FunctionDeclaration':
case 'ArrowFunctionExpression':
case 'BlockStatement':
scope = scope.parent;
break;
} }
} }
}); });
@ -112,6 +122,11 @@ export default function analyse ( ast, code ) {
ast.body.forEach( statement => { ast.body.forEach( statement => {
function checkForReads ( node, parent ) { function checkForReads ( node, parent ) {
if ( node.type === 'Identifier' ) { if ( node.type === 'Identifier' ) {
// disregard the `bar` in `foo.bar` - these appear as Identifier nodes
if ( parent.type === 'MemberExpression' && node !== parent.object ) {
return;
}
const definingScope = scope.findDefiningScope( node.name ); const definingScope = scope.findDefiningScope( node.name );
if ( ( !definingScope || definingScope.depth === 0 ) && !statement._defines[ node.name ] ) { if ( ( !definingScope || definingScope.depth === 0 ) && !statement._defines[ node.name ] ) {

3
src/finalisers/amd.js

@ -0,0 +1,3 @@
export default function amd ( bundle, magicString, options ) {
throw new Error( 'TODO' );
}

3
src/finalisers/cjs.js

@ -0,0 +1,3 @@
export default function cjs ( bundle, magicString, options ) {
return magicString;
}

3
src/finalisers/es6.js

@ -0,0 +1,3 @@
export default function es6 ( bundle, magicString, options ) {
throw new Error( 'TODO' );
}

6
src/finalisers/index.js

@ -0,0 +1,6 @@
import amd from './amd';
import cjs from './cjs';
import es6 from './es6';
import umd from './umd';
export default { amd, cjs, es6, umd };

24
src/finalisers/umd.js

@ -0,0 +1,24 @@
export default function umd ( bundle, magicString, options ) {
const indentStr = magicString.getIndentString();
const intro =
`(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(factory) :
factory((global.${options.globalName} = {}));
}(this, function (exports) { 'use strict';
`.replace( /^\t\t/gm, '' ).replace( /^\t/g, indentStr );
const exports = bundle.entryModule.exports;
const exportBlock = '\n\n' + Object.keys( exports ).map( name => {
return `exports.${name} = ${exports[name].localName};`
}).join( '\n' );
return magicString
.append( exportBlock )
.indent()
.append( '\n\n}));' )
.prepend( intro );
}

7
src/rollup.js

@ -1,3 +1,4 @@
import { writeFile } from 'sander';
import Bundle from './Bundle'; import Bundle from './Bundle';
export function rollup ( entry, options = {} ) { export function rollup ( entry, options = {} ) {
@ -9,8 +10,10 @@ export function rollup ( entry, options = {} ) {
return bundle.collect().then( () => { return bundle.collect().then( () => {
return { return {
generate: options => bundle.generate( options ), generate: options => bundle.generate( options ),
write: () => { write: ( dest, options ) => {
throw new Error( 'TODO' ); const generated = bundle.generate( options );
return writeFile( dest, generated.code );
} }
}; };
}); });

3
test/samples/import-default-function/_config.js

@ -1,3 +1,4 @@
module.exports = { module.exports = {
description: 'imports a default function' description: 'imports a default function',
// solo: true
}; };

3
test/samples/try-catch-scoping/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'error parameter in catch clause is correctly scoped'
};

7
test/samples/try-catch-scoping/foo.js

@ -0,0 +1,7 @@
export default function foo () {
try {
return 42;
} catch ( err ) {
console.log( err );
}
}

2
test/samples/try-catch-scoping/main.js

@ -0,0 +1,2 @@
import foo from './foo';
assert.equal( foo(), 42 );
Loading…
Cancel
Save