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 { keys, has } from './utils/object';
import { sequence } from './utils/promise'; import { sequence } from './utils/promise';
import Module from './Module'; import Module from './Module';
import ExternalModule from './ExternalModule';
import finalisers from './finalisers/index'; import finalisers from './finalisers/index';
import replaceIdentifiers from './utils/replaceIdentifiers'; import replaceIdentifiers from './utils/replaceIdentifiers';
import makeLegalIdentifier from './utils/makeLegalIdentifier';
export default class Bundle { export default class Bundle {
constructor ( options ) { constructor ( options ) {
@ -27,6 +29,9 @@ export default class Bundle {
} }
fetchModule ( path, id ) { fetchModule ( path, id ) {
// TODO currently, we'll get different ExternalModule objects
// depending on where they're imported from...
if ( !has( this.modulePromises, path ) ) { if ( !has( this.modulePromises, path ) ) {
this.modulePromises[ path ] = readFile( path, { encoding: 'utf-8' }) this.modulePromises[ path ] = readFile( path, { encoding: 'utf-8' })
.then( code => { .then( code => {
@ -48,11 +53,7 @@ export default class Bundle {
// most likely an external module // most likely an external module
// TODO fire an event, or otherwise allow some way for // TODO fire an event, or otherwise allow some way for
// users to control external modules better? // users to control external modules better?
const module = { const module = new ExternalModule( id );
id,
isExternal: true,
specifiers: []
};
this.externalModules.push( module ); this.externalModules.push( module );
return module; return module;
@ -63,14 +64,6 @@ export default class Bundle {
return this.modulePromises[ path ]; return this.modulePromises[ path ];
} }
getBindingNamesFor ( module ) {
if ( !has( this.bindingNames, module.path ) ) {
this.bindingNames[ module.path ] = {};
}
return this.bindingNames[ module.path ];
}
build () { build () {
// bring in top-level AST nodes from the entry module // bring in top-level AST nodes from the entry module
return this.fetchModule( this.entryPath ) return this.fetchModule( this.entryPath )
@ -108,6 +101,18 @@ export default class Bundle {
} }
deconflict () { 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 definers = {};
let conflicts = {}; 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 // Rename conflicting identifiers so they can live in the same scope
keys( conflicts ).forEach( name => { keys( conflicts ).forEach( name => {
const modules = definers[ name ]; const modules = definers[ name ];
@ -156,12 +172,27 @@ export default class Bundle {
} }
generate ( options = {} ) { 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 => { this.body.forEach( statement => {
const module = statement._module;
replaceIdentifiers( statement, statement._source, module.nameReplacements );
magicString.addSource( statement._source ); 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.definedNames = this.ast._scope.names.slice();
this.nameReplacements = {};
this.canonicalNames = {}; this.canonicalNames = {};
this.definitions = {}; this.definitions = {};
@ -49,9 +48,13 @@ export default class Module {
}); });
}); });
// imports and exports, indexed by ID
this.imports = {}; this.imports = {};
this.exports = {}; this.exports = {};
// an array of export statements, used for the entry module
this.exportStatements = [];
this.ast.body.forEach( node => { this.ast.body.forEach( node => {
// import foo from './foo'; // import foo from './foo';
// import { bar } from './bar'; // import { bar } from './bar';
@ -59,65 +62,73 @@ export default class Module {
const source = node.source.value; const source = node.source.value;
node.specifiers.forEach( specifier => { node.specifiers.forEach( specifier => {
const name = specifier.local.name;
const isDefault = specifier.type === 'ImportDefaultSpecifier'; 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, source,
name: isDefault ? 'default' : specifier.imported.name, name,
localName: name localName
}; };
}); });
} }
// export default function foo () {} else if ( /^Export/.test( node.type ) ) {
// export default foo; this.exportStatements.push( node );
// 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
};
}
// export { foo, bar, baz } // export default function foo () {}
// export var foo = 42; // export default foo;
// export function foo () {} // export default 42;
else if ( node.type === 'ExportNamedDeclaration' ) { if ( node.type === 'ExportDefaultDeclaration' ) {
if ( node.specifiers.length ) { const isDeclaration = /Declaration$/.test( node.declaration.type );
// export { foo, bar, baz }
node.specifiers.forEach( specifier => { this.exports.default = {
const localName = specifier.local.name; node,
const exportedName = specifier.exported.name; name: 'default',
localName: isDeclaration ? node.declaration.id.name : 'default',
this.exports[ exportedName ] = { isDeclaration
localName };
};
});
} }
else { // export { foo, bar, baz }
let declaration = node.declaration; // 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' ) { let name;
// export var foo = 42
name = declaration.declarations[0].id.name;
} else {
// export function foo () {}
name = declaration.id.name;
}
this.exports[ name ] = { if ( declaration.type === 'VariableDeclaration' ) {
localName: name, // export var foo = 42
expression: declaration 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 importDeclaration = this.imports[ name ];
const module = importDeclaration.module; const module = importDeclaration.module;
// TODO handle external modules let exporterLocalName;
const exportDeclaration = module.exports[ importDeclaration.name ];
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' ) { if ( name === 'default' ) {
@ -156,23 +173,23 @@ export default class Module {
promise = this.bundle.fetchModule( path, importDeclaration.source ) promise = this.bundle.fetchModule( path, importDeclaration.source )
.then( module => { .then( module => {
importDeclaration.module = module;
if ( importDeclaration.name === 'default' ) {
module.suggestDefaultName( importDeclaration.localName );
}
if ( module.isExternal ) { if ( module.isExternal ) {
module.specifiers.push( importDeclaration ); module.importedByBundle.push( importDeclaration );
return emptyArrayPromise; return emptyArrayPromise;
} }
importDeclaration.module = module;
const exportDeclaration = module.exports[ importDeclaration.name ]; const exportDeclaration = module.exports[ importDeclaration.name ];
if ( !exportDeclaration ) { if ( !exportDeclaration ) {
throw new Error( `Module ${module.path} does not export ${importDeclaration.name} (imported by ${this.path})` ); 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 ); 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 ) { 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 if ( exportBlock ) {
bundle.externalModules.forEach( module => { magicString.append( '\n\n' + exportBlock );
console.log( 'module', module ); }
//intro += `var
});
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 { try {
var fn = new Function( 'assert', result.code ); var fn = new Function( 'require', 'exports', 'assert', result.code );
fn( assert ); var exports = {};
fn( require, exports, assert );
if ( config.exports ) {
config.exports( exports, assert );
}
} catch ( err ) { } catch ( err ) {
console.log( result.code ); console.log( result.code );
throw err; throw err;

Loading…
Cancel
Save