Browse Source

basic export handling

contingency-plan
Rich-Harris 10 years ago
parent
commit
c93ce90ca7
  1. 65
      src/Bundle.js
  2. 33
      src/ExternalModule.js
  3. 131
      src/Module.js
  4. 35
      src/finalisers/cjs.js
  5. 6
      src/utils/makeLegalIdentifier.js
  6. 3
      test/samples/allows-external-modules/_config.js
  7. 6
      test/samples/allows-external-modules/main.js
  8. 6
      test/samples/exports-named-values/_config.js
  9. 1
      test/samples/exports-named-values/main.js
  10. 9
      test/test.js

65
src/Bundle.js

@ -4,8 +4,10 @@ import MagicString from 'magic-string';
import { keys, has } from './utils/object';
import { sequence } from './utils/promise';
import Module from './Module';
import ExternalModule from './ExternalModule';
import finalisers from './finalisers/index';
import replaceIdentifiers from './utils/replaceIdentifiers';
import makeLegalIdentifier from './utils/makeLegalIdentifier';
export default class Bundle {
constructor ( options ) {
@ -27,6 +29,9 @@ export default class Bundle {
}
fetchModule ( path, id ) {
// TODO currently, we'll get different ExternalModule objects
// depending on where they're imported from...
if ( !has( this.modulePromises, path ) ) {
this.modulePromises[ path ] = readFile( path, { encoding: 'utf-8' })
.then( code => {
@ -48,11 +53,7 @@ export default class Bundle {
// most likely an external module
// TODO fire an event, or otherwise allow some way for
// users to control external modules better?
const module = {
id,
isExternal: true,
specifiers: []
};
const module = new ExternalModule( id );
this.externalModules.push( module );
return module;
@ -63,14 +64,6 @@ export default class Bundle {
return this.modulePromises[ path ];
}
getBindingNamesFor ( module ) {
if ( !has( this.bindingNames, module.path ) ) {
this.bindingNames[ module.path ] = {};
}
return this.bindingNames[ module.path ];
}
build () {
// bring in top-level AST nodes from the entry module
return this.fetchModule( this.entryPath )
@ -108,6 +101,18 @@ export default class Bundle {
}
deconflict () {
// TODO this probably needs to happen at generate time, since
// treatment of external modules differs between formats
// e.g. this...
//
// import { relative } from 'path'`;
// console.log( relative( 'foo', 'bar' ) );
//
// ...would look very similar when bundled as ES6, but in
// a CommonJS bundle would become this:
//
// var path = require( 'path' );
// console.log( path.relative( 'foo', 'bar' ) );
let definers = {};
let conflicts = {};
@ -126,6 +131,17 @@ export default class Bundle {
});
});
// Assign names to external modules
this.externalModules.forEach( module => {
let name = makeLegalIdentifier( module.id );
while ( has( definers, name ) ) {
name = `_${name}`;
}
module.name = name;
});
// Rename conflicting identifiers so they can live in the same scope
keys( conflicts ).forEach( name => {
const modules = definers[ name ];
@ -156,12 +172,27 @@ export default class Bundle {
}
generate ( options = {} ) {
let magicString = new MagicString.Bundle();
let magicString = new MagicString.Bundle({ separator: '' });
this.entryModule.exportStatements.forEach( statement => {
if ( statement.specifiers.length ) {
// we don't need to include `export { foo }`, it's already handled
return;
}
if ( statement.declaration.type === 'VariableDeclaration' ) {
const declarator = statement.declaration.declarations[0];
statement._source.remove( statement.start, statement.declaration.start );
} else {
// TODO function, class declarations
}
// TODO are there situations where the export needs to be
// placed higher up, i.e. kept in situ? probably...
this.body.push( statement );
});
this.body.forEach( statement => {
const module = statement._module;
replaceIdentifiers( statement, statement._source, module.nameReplacements );
magicString.addSource( statement._source );
});

33
src/ExternalModule.js

@ -0,0 +1,33 @@
import { has } from './utils/object';
export default class ExternalModule {
constructor ( id ) {
this.id = id;
this.name = null;
this.isExternal = true;
this.importedByBundle = [];
this.canonicalNames = {};
this.defaultExportName = null;
}
getCanonicalName ( name ) {
if ( name === 'default' ) {
return `${this.name}__default`; // TODO...
}
// TODO this depends on the output format... works for CJS etc but not ES6
return `${this.name}.${name}`;
}
rename ( name, replacement ) {
this.canonicalNames[ name ] = replacement;
}
suggestDefaultName ( name ) {
if ( !this.defaultExportName ) {
this.defaultExportName = name;
}
}
}

131
src/Module.js

@ -28,7 +28,6 @@ export default class Module {
this.definedNames = this.ast._scope.names.slice();
this.nameReplacements = {};
this.canonicalNames = {};
this.definitions = {};
@ -49,9 +48,13 @@ export default class Module {
});
});
// imports and exports, indexed by ID
this.imports = {};
this.exports = {};
// an array of export statements, used for the entry module
this.exportStatements = [];
this.ast.body.forEach( node => {
// import foo from './foo';
// import { bar } from './bar';
@ -59,65 +62,73 @@ export default class Module {
const source = node.source.value;
node.specifiers.forEach( specifier => {
const name = specifier.local.name;
const isDefault = specifier.type === 'ImportDefaultSpecifier';
const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
const localName = specifier.local.name;
const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
this.imports[ name ] = {
this.imports[ localName ] = {
source,
name: isDefault ? 'default' : specifier.imported.name,
localName: name
name,
localName
};
});
}
// export default function foo () {}
// export default foo;
// export default 42;
else if ( node.type === 'ExportDefaultDeclaration' ) {
const isDeclaration = /Declaration$/.test( node.declaration.type );
this.exports.default = {
node,
name: 'default',
localName: isDeclaration ? node.declaration.id.name : 'default',
isDeclaration,
module: null // filled in later
};
}
else if ( /^Export/.test( node.type ) ) {
this.exportStatements.push( node );
// 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[ exportedName ] = {
localName
};
});
// export default function foo () {}
// export default foo;
// export default 42;
if ( node.type === 'ExportDefaultDeclaration' ) {
const isDeclaration = /Declaration$/.test( node.declaration.type );
this.exports.default = {
node,
name: 'default',
localName: isDeclaration ? node.declaration.id.name : 'default',
isDeclaration
};
}
else {
let declaration = node.declaration;
// 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[ exportedName ] = {
localName,
exportedName
};
});
}
let name;
else {
let declaration = node.declaration;
if ( declaration.type === 'VariableDeclaration' ) {
// export var foo = 42
name = declaration.declarations[0].id.name;
} else {
// export function foo () {}
name = declaration.id.name;
}
let name;
this.exports[ name ] = {
localName: name,
expression: declaration
};
if ( declaration.type === 'VariableDeclaration' ) {
// export var foo = 42
name = declaration.declarations[0].id.name;
} else {
// export function foo () {}
name = declaration.id.name;
}
this.exports[ name ] = {
node,
localName: name,
expression: declaration
};
}
}
}
});
@ -128,10 +139,16 @@ export default class Module {
const importDeclaration = this.imports[ name ];
const module = importDeclaration.module;
// TODO handle external modules
const exportDeclaration = module.exports[ importDeclaration.name ];
let exporterLocalName;
return module.getCanonicalName( exportDeclaration.localName );
if ( module.isExternal ) {
exporterLocalName = name;
} else {
const exportDeclaration = module.exports[ importDeclaration.name ];
exporterLocalName = exportDeclaration.localName;
}
return module.getCanonicalName( exporterLocalName );
}
if ( name === 'default' ) {
@ -156,23 +173,23 @@ export default class Module {
promise = this.bundle.fetchModule( path, importDeclaration.source )
.then( module => {
importDeclaration.module = module;
if ( importDeclaration.name === 'default' ) {
module.suggestDefaultName( importDeclaration.localName );
}
if ( module.isExternal ) {
module.specifiers.push( importDeclaration );
module.importedByBundle.push( importDeclaration );
return emptyArrayPromise;
}
importDeclaration.module = module;
const exportDeclaration = module.exports[ importDeclaration.name ];
if ( !exportDeclaration ) {
throw new Error( `Module ${module.path} does not export ${importDeclaration.name} (imported by ${this.path})` );
}
if ( importDeclaration.name === 'default' ) {
module.suggestDefaultName( importDeclaration.localName );
}
return module.define( exportDeclaration.localName );
});
}

35
src/finalisers/cjs.js

@ -1,11 +1,32 @@
import { keys } from '../utils/object';
import makeLegalIdentifier from '../utils/makeLegalIdentifier';
export default function cjs ( bundle, magicString, options ) {
const intro = `'use strict';\n\n`;
let intro = `'use strict';\n\n`;
// TODO handle ambiguous default imports
// TODO handle empty imports, once they're supported
const importBlock = bundle.externalModules
.map( module => `var ${module.name} = require('${module.id}');` )
.join( '\n' );
if ( importBlock ) {
intro += importBlock + '\n\n';
}
magicString.prepend( intro );
// TODO handle default exports
const exportBlock = keys( bundle.entryModule.exports )
.map( key => {
const specifier = bundle.entryModule.exports[ key ];
return `exports.${key} = ${specifier.localName}`;
})
.join( '\n' );
// TODO group modules more intelligently
bundle.externalModules.forEach( module => {
console.log( 'module', module );
//intro += `var
});
if ( exportBlock ) {
magicString.append( '\n\n' + exportBlock );
}
return magicString.prepend( intro );
return magicString;
}

6
src/utils/makeLegalIdentifier.js

@ -0,0 +1,6 @@
export default function makeLegalIdentifier ( str ) {
str = str.replace( /[^$_a-zA-Z0-9]/g, '_' );
if ( /\d/.test( str[0] ) ) str = `_${str}`;
return str;
}

3
test/samples/allows-external-modules/_config.js

@ -0,0 +1,3 @@
module.exports = {
description: 'Non-existent modules are assumed to be external'
};

6
test/samples/allows-external-modules/main.js

@ -0,0 +1,6 @@
import { relative } from 'path';
var path = 'foo/bar/baz';
var path2 = 'foo/baz/bar';
assert.equal( relative( path, path2 ), '../../baz/bar' );

6
test/samples/exports-named-values/_config.js

@ -0,0 +1,6 @@
module.exports = {
description: 'exports named values from the bundle entry module',
exports: function ( exports, assert ) {
assert.equal( exports.answer, 42 );
}
};

1
test/samples/exports-named-values/main.js

@ -0,0 +1 @@
export var answer = 42;

9
test/test.js

@ -30,8 +30,13 @@ describe( 'rollup', function () {
});
try {
var fn = new Function( 'assert', result.code );
fn( assert );
var fn = new Function( 'require', 'exports', 'assert', result.code );
var exports = {};
fn( require, exports, assert );
if ( config.exports ) {
config.exports( exports, assert );
}
} catch ( err ) {
console.log( result.code );
throw err;

Loading…
Cancel
Save