From 9ec400a2a08834b0e8bc5b45ffc22e058d1226c5 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Fri, 15 May 2015 22:10:51 -0400 Subject: [PATCH] replace names as necessary --- src/Bundle/index.js | 60 +-------------- src/Module/index.js | 73 +++++++++++++------ src/ast/analyse.js | 18 ++++- src/rollup.js | 13 +++- src/utils/replaceIdentifiers.js | 55 ++++++++++++++ .../import-default-as-other/_config.js | 3 + test/samples/import-default-as-other/foo.js | 3 + test/samples/import-default-as-other/main.js | 2 + .../import-named-function-as-other/_config.js | 3 + .../import-named-function-as-other/foo.js | 3 + .../import-named-function-as-other/main.js | 2 + 11 files changed, 152 insertions(+), 83 deletions(-) create mode 100644 src/utils/replaceIdentifiers.js create mode 100644 test/samples/import-default-as-other/_config.js create mode 100644 test/samples/import-default-as-other/foo.js create mode 100644 test/samples/import-default-as-other/main.js create mode 100644 test/samples/import-named-function-as-other/_config.js create mode 100644 test/samples/import-named-function-as-other/foo.js create mode 100644 test/samples/import-named-function-as-other/main.js diff --git a/src/Bundle/index.js b/src/Bundle/index.js index a3ce015..e80cfb7 100644 --- a/src/Bundle/index.js +++ b/src/Bundle/index.js @@ -91,63 +91,9 @@ export default class Bundle { return { code: magicString.toString(), - map: null // TODO use magicString.generateMap() - }; - - // try { - // const code = generate({ - // type: 'Program', - // body: this.body - // }); - - // const code = this.body.map( statement => statement._source.toString ).join( '\n' ); - - // return { - // code, - // map: null // TODO... - // }; - // } catch ( err ) { - // // probably an escodegen error - // console.log( 'this.body', this.body ); - // console.log( 'err.stack', err.stack ); - // throw err; - // } - } - - getName ( module, localName ) { - if ( !hasOwnProp.call( this.names, module.path ) ) { - this.names[ module.path ] = {}; - } - - const moduleNames = this.names[ module.path ]; - - if ( !moduleNames ) { - throw new Error( `Could not get name for ${module.relativePath}:${localName}` ); - } - - return moduleNames[ localName ]; - } - - suggestName ( module, localName, globalName ) { - if ( !hasOwnProp.call( this.names, module.path ) ) { - this.names[ module.path ] = {}; - } - - const moduleNames = this.names[ module.path ]; - - if ( !hasOwnProp.call( moduleNames, globalName ) ) { - const relativePathParts = module.relativePath.split( sep ); + map: magicString.generateMap({ - while ( hasOwnProp.call( this.usedNames, globalName ) && relativePathParts.length ) { - globalName = relativePathParts.pop() + `__${globalName}`; - } - - while ( hasOwnProp.call( this.usedNames, globalName ) ) { - globalName = `_${globalName}`; - } - - this.usedNames[ globalName ] = true; - moduleNames[ localName ] = globalName; - } + }) + }; } } \ No newline at end of file diff --git a/src/Module/index.js b/src/Module/index.js index 13e1c04..f5c897c 100644 --- a/src/Module/index.js +++ b/src/Module/index.js @@ -5,6 +5,7 @@ import MagicString from 'magic-string'; import analyse from '../ast/analyse'; import { hasOwnProp } from '../utils/object'; import { sequence } from '../utils/promise'; +import replaceIdentifiers from '../utils/replaceIdentifiers'; const emptyArrayPromise = Promise.resolve([]); @@ -20,9 +21,9 @@ export default class Module { sourceType: 'module' }); - console.log( '\nanalysing %s\n========', path ); analyse( this.ast, this.code ); - console.log( '========\n\n' ); + + this.nameReplacements = {}; this.definitions = {}; this.definitionPromises = {}; @@ -34,7 +35,11 @@ export default class Module { }); Object.keys( statement._modifies ).forEach( name => { - this.modifications[ name ] = statement; + if ( !hasOwnProp.call( this.modifications, name ) ) { + this.modifications[ name ] = []; + } + + this.modifications[ name ].push( statement ); }); }); @@ -62,8 +67,8 @@ export default class Module { this.exports.default = { node, - localName: 'default', - name: isDeclaration ? node.declaration.id.name : null, + name: 'default', + localName: isDeclaration ? node.declaration.id.name : 'default', isDeclaration }; } @@ -128,7 +133,7 @@ export default class Module { // 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 ); + module.nameReplacements[ exportDeclaration.localName ] = importDeclaration.localName; return module.define( exportDeclaration.localName ); }); @@ -137,22 +142,27 @@ export default class Module { // 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 + // searching for `foo` instead of default. First, sync up names + this.nameReplacements.default = this.exports.default.name; promise = this.define( this.exports.default.name ); } else { let statement; + if ( !name ) { + console.log( new Error( 'no name' ).stack ); + } + 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' ); + const replacement = this.nameReplacements.default; statement = this.exports.default.node; if ( !statement._imported ) { - statement._source.overwrite( statement.start, statement.declaration.start, `var ${name} = ` ) + statement._source.overwrite( statement.start, statement.declaration.start, `var ${replacement} = ` ) } } @@ -167,19 +177,34 @@ export default class Module { if ( statement && !statement._imported ) { const nodes = []; - 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; - }); + // replace identifiers, as necessary + replaceIdentifiers( statement, statement._source, this.nameReplacements ); + + const include = statement => { + if ( statement._imported ) return emptyArrayPromise; + + const dependencies = Object.keys( statement._dependsOn ); + + return sequence( dependencies, name => this.define( name ) ) + .then( definitions => { + definitions.forEach( definition => nodes.push.apply( nodes, definition ) ); + }) + .then( () => { + statement._imported = true; + nodes.push( statement ); + + const modifications = hasOwnProp.call( this.modifications, name ) && this.modifications[ name ]; + + if ( modifications ) { + return sequence( modifications, include ); + } + }) + .then( () => { + return nodes; + }); + }; + + promise = include( statement ); } } @@ -188,4 +213,8 @@ export default class Module { return this.definitionPromises[ name ]; } + + replaceName ( name, replacement ) { + this.nameReplacements[ name ] = replacement; + } } \ No newline at end of file diff --git a/src/ast/analyse.js b/src/ast/analyse.js index fa80fe3..4c63c3d 100644 --- a/src/ast/analyse.js +++ b/src/ast/analyse.js @@ -9,7 +9,7 @@ function isStatement ( node, parent ) { node.type === 'FunctionDeclaration'; // TODO or any of the other various statement-ish things it could be } -export default function analyse ( ast, code ) { +export default function analyse ( ast, magicString ) { let scope = new Scope(); let topLevelStatements = []; let currentTopLevelStatement; @@ -44,7 +44,7 @@ export default function analyse ( ast, code ) { statement._imported = false; // store the actual code, for easy regeneration - statement._source = code.snip( previous, statement.end ); + statement._source = magicString.snip( previous, statement.end ); previous = statement.end; currentTopLevelStatement = statement; // so we can attach scoping info @@ -53,6 +53,8 @@ export default function analyse ( ast, code ) { enter ( node, parent ) { let newScope; + magicString.addSourcemapLocation( node.start ); + switch ( node.type ) { case 'FunctionExpression': case 'FunctionDeclaration': @@ -137,7 +139,19 @@ export default function analyse ( ast, code ) { } function checkForWrites ( node ) { + if ( node.type === 'AssignmentExpression' ) { + let assignee = node.left; + + while ( assignee.type === 'MemberExpression' ) { + assignee = assignee.object; + } + if ( assignee.type !== 'Identifier' ) { // could be a ThisExpression + return; + } + + statement._modifies[ assignee.name ] = true; + } } walk( statement, { diff --git a/src/rollup.js b/src/rollup.js index 1e8591a..e99709a 100644 --- a/src/rollup.js +++ b/src/rollup.js @@ -1,6 +1,10 @@ +import { basename } from 'path'; import { writeFile } from 'sander'; import Bundle from './Bundle'; +let SOURCEMAPPING_URL = 'sourceMa'; +SOURCEMAPPING_URL += 'ppingURL'; + export function rollup ( entry, options = {} ) { const bundle = new Bundle({ entry, @@ -11,9 +15,14 @@ export function rollup ( entry, options = {} ) { return { generate: options => bundle.generate( options ), write: ( dest, options ) => { - const generated = bundle.generate( options ); + let { code, map } = bundle.generate( options ); + + code += `\n//# ${SOURCEMAPPING_URL}=${basename( dest )}.map`; - return writeFile( dest, generated.code ); + return Promise.all([ + writeFile( dest, code ), + writeFile( dest + '.map', map.toString() ) + ]); } }; }); diff --git a/src/utils/replaceIdentifiers.js b/src/utils/replaceIdentifiers.js new file mode 100644 index 0000000..15d8e02 --- /dev/null +++ b/src/utils/replaceIdentifiers.js @@ -0,0 +1,55 @@ +import walk from '../ast/walk'; +import { hasOwnProp } from './object'; + +export default function replaceIdentifiers ( statement, snippet, names ) { + const replacementStack = [ names ]; + + const keys = Object.keys( names ); + + if ( keys.length === 0 ) { + return; + } + + walk( statement, { + enter ( node, parent ) { + const scope = node._scope; + + if ( scope ) { + let newNames = {}; + let hasReplacements; + + keys.forEach( key => { + if ( !~scope.names.indexOf( key ) ) { + newNames[ key ] = names[ key ]; + hasReplacements = true; + } + }); + + if ( !hasReplacements ) { + return this.skip(); + } + + replacementStack.push( newNames ); + } + + if ( node.type === 'Identifier' && parent.type !== 'MemberExpression' ) { + let name = node.name; + + while ( hasOwnProp.call( names, name ) && name !== names[ name ] ) { + name = names[ name ]; + } + + if ( name && name !== node.name ) { + snippet.overwrite( node.start, node.end, name ); + } + } + }, + + leave ( node ) { + if ( node._scope ) { + replacementStack.pop(); + names = replacementStack[ replacementStack.length - 1 ]; + } + } + }); +} \ No newline at end of file diff --git a/test/samples/import-default-as-other/_config.js b/test/samples/import-default-as-other/_config.js new file mode 100644 index 0000000..bac0d45 --- /dev/null +++ b/test/samples/import-default-as-other/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'imports a default import by another name' +}; \ No newline at end of file diff --git a/test/samples/import-default-as-other/foo.js b/test/samples/import-default-as-other/foo.js new file mode 100644 index 0000000..27e9d21 --- /dev/null +++ b/test/samples/import-default-as-other/foo.js @@ -0,0 +1,3 @@ +export default function foo () { + return 42; +} \ No newline at end of file diff --git a/test/samples/import-default-as-other/main.js b/test/samples/import-default-as-other/main.js new file mode 100644 index 0000000..605d432 --- /dev/null +++ b/test/samples/import-default-as-other/main.js @@ -0,0 +1,2 @@ +import bar from './foo'; +assert.equal( bar(), 42 ); \ No newline at end of file diff --git a/test/samples/import-named-function-as-other/_config.js b/test/samples/import-named-function-as-other/_config.js new file mode 100644 index 0000000..53d3c85 --- /dev/null +++ b/test/samples/import-named-function-as-other/_config.js @@ -0,0 +1,3 @@ +module.exports = { + description: 'imports a named function by another name' +}; \ No newline at end of file diff --git a/test/samples/import-named-function-as-other/foo.js b/test/samples/import-named-function-as-other/foo.js new file mode 100644 index 0000000..98c6f1f --- /dev/null +++ b/test/samples/import-named-function-as-other/foo.js @@ -0,0 +1,3 @@ +export function foo () { + return 42; +} \ No newline at end of file diff --git a/test/samples/import-named-function-as-other/main.js b/test/samples/import-named-function-as-other/main.js new file mode 100644 index 0000000..754b140 --- /dev/null +++ b/test/samples/import-named-function-as-other/main.js @@ -0,0 +1,2 @@ +import { foo as bar } from './foo'; +assert.equal( bar(), 42 ); \ No newline at end of file