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. 75
      src/Module/index.js
  4. 43
      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",
"version": "0.1.0",
"description": "Next-generation ES6 module bundler",
"main": "dist/index.js",
"jsnext:main": "src/index.js",
"main": "dist/rollup.js",
"jsnext:main": "src/rollup.js",
"scripts": {
"test": "mocha",
"pretest": "npm run build",

21
src/Bundle/index.js

@ -4,6 +4,7 @@ import MagicString from 'magic-string';
import { hasOwnProp } from '../utils/object';
import { sequence } from '../utils/promise';
import Module from '../Module/index';
import finalisers from '../finalisers/index';
export default class Bundle {
constructor ( options ) {
@ -20,6 +21,8 @@ export default class Bundle {
// this will store per-module names, and enable deconflicting
this.names = {};
this.usedNames = {};
this.externalModules = [];
}
collect () {
@ -71,16 +74,24 @@ export default class Bundle {
});
}
generate () {
const bundle = new MagicString.Bundle();
generate ( options = {} ) {
let magicString = new MagicString.Bundle();
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 {
code: bundle.toString(),
map: null // TODO use bundle.generateMap()
code: magicString.toString(),
map: null // TODO use magicString.generateMap()
};
// try {

75
src/Module/index.js

@ -1,14 +1,17 @@
import { dirname, relative, resolve } from 'path';
import { Promise } from 'sander';
import { parse } from 'acorn';
import MagicString from 'magic-string';
import analyse from '../ast/analyse';
import { hasOwnProp } from '../utils/object';
import { sequence } from '../utils/promise';
const emptyArrayPromise = Promise.resolve([]);
export default class Module {
constructor ({ path, code, bundle }) {
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.bundle = bundle;
@ -17,9 +20,12 @@ export default class Module {
sourceType: 'module'
});
console.log( '\nanalysing %s\n========', path );
analyse( this.ast, this.code );
console.log( '========\n\n' );
this.definitions = {};
this.definitionPromises = {};
this.modifications = {};
this.ast.body.forEach( statement => {
@ -52,20 +58,14 @@ export default class Module {
}
else if ( node.type === 'ExportDefaultDeclaration' ) {
//const isDeclaration = /Declaration$/.test( node)
const isDeclaration = /Declaration$/.test( node.declaration.type );
this.exports.default = {
node,
localName: 'default',
isDeclaration: false,
//expression: node.declaration
name: isDeclaration ? node.declaration.id.name : null,
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' ) {
@ -103,15 +103,21 @@ export default class Module {
});
}
define ( name, importer ) {
let statement;
define ( name ) {
// shortcut cycles. TODO this won't work everywhere...
if ( hasOwnProp.call( this.definitionPromises, name ) ) {
return emptyArrayPromise;
}
if ( !hasOwnProp.call( this.definitionPromises, name ) ) {
let promise;
// The definition for this name is in a different module
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 )
promise = this.bundle.fetchModule( path )
.then( module => {
const exportDeclaration = module.exports[ importDeclaration.name ];
@ -124,55 +130,62 @@ export default class Module {
// with something slightly different
this.bundle.suggestName( module, exportDeclaration.localName, importDeclaration.localName );
return module.define( exportDeclaration.localName, this );
return module.define( exportDeclaration.localName );
});
}
// The definition is in this module; it's the default export
else if ( name === 'default' ) {
const defaultExport = this.exports.default;
// 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
if ( defaultExport.isDeclaration ) {
return this.define( defaultExport.name, this );
promise = this.define( this.exports.default.name );
}
// Otherwise, we have an expression, e.g. `export default 42`. We have
else {
let statement;
if ( name === 'default' ) {
// 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' );
statement = defaultExport.node;
statement = this.exports.default.node;
if ( !statement._imported ) {
statement._source.overwrite( statement.start, statement.declaration.start, `var ${name} = ` )
}
}
else {
statement = this.definitions[ name ];
if ( /^Export/.test( statement.type ) ) {
if ( statement && /^Export/.test( statement.type ) ) {
statement._source.remove( statement.start, statement.declaration.start );
}
}
if ( statement ) {
if ( statement && !statement._imported ) {
const nodes = [];
return sequence( Object.keys( statement._dependsOn ), name => {
return this.define( name, this );
promise = sequence( Object.keys( statement._dependsOn ), name => {
return this.define( name );
})
.then( definitions => {
definitions.forEach( definition => nodes.push.apply( nodes, definition ) );
})
.then( () => {
statement._imported = true;
nodes.push( statement );
})
.then( () => {
return nodes;
});
} else {
throw new Error( `Could not define ${name}` );
}
}
this.definitionPromises[ name ] = promise || emptyArrayPromise;
}
return this.definitionPromises[ name ];
}
}

43
src/ast/analyse.js

@ -41,6 +41,7 @@ export default function analyse ( ast, code ) {
statement._defines = {};
statement._modifies = {};
statement._dependsOn = {};
statement._imported = false;
// store the actual code, for easy regeneration
statement._source = code.snip( previous, statement.end );
@ -50,33 +51,42 @@ export default function analyse ( ast, code ) {
walk( statement, {
enter ( node, parent ) {
let newScope;
switch ( node.type ) {
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression':
if ( node.id ) {
let names = node.params.map( getName );
if ( node.type === 'FunctionDeclaration' ) {
addToScope( node );
} else if ( node.type === 'FunctionExpression' && node.id ) {
names.push( node.id.name );
}
let names = node.params.map( getName );
scope = new Scope({
newScope = new Scope({
parent: scope,
params: names, // TODO rest params?
block: false
});
Object.defineProperty( node, '_scope', { value: scope });
break;
case 'BlockStatement':
scope = new Scope({
newScope = new Scope({
parent: scope,
block: true
});
Object.defineProperty( node, '_scope', { value: scope });
break;
case 'CatchClause':
newScope = new Scope({
parent: scope,
params: [ node.param.name ],
block: true
});
break;
@ -89,19 +99,19 @@ export default function analyse ( ast, code ) {
addToScope( node );
break;
}
if ( newScope ) {
Object.defineProperty( node, '_scope', { value: newScope });
scope = newScope;
}
},
leave ( node ) {
if ( node === currentTopLevelStatement ) {
currentTopLevelStatement = null;
}
switch ( node.type ) {
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression':
case 'BlockStatement':
if ( node._scope ) {
scope = scope.parent;
break;
}
}
});
@ -112,6 +122,11 @@ export default function analyse ( ast, code ) {
ast.body.forEach( statement => {
function checkForReads ( node, parent ) {
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 );
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';
export function rollup ( entry, options = {} ) {
@ -9,8 +10,10 @@ export function rollup ( entry, options = {} ) {
return bundle.collect().then( () => {
return {
generate: options => bundle.generate( options ),
write: () => {
throw new Error( 'TODO' );
write: ( dest, options ) => {
const generated = bundle.generate( options );
return writeFile( dest, generated.code );
}
};
});

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

@ -1,3 +1,4 @@
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