|
|
@ -1,10 +1,5 @@ |
|
|
|
import { walk } from 'estree-walker'; |
|
|
|
import { keys } from './utils/object'; |
|
|
|
|
|
|
|
const modifierNodes = { |
|
|
|
AssignmentExpression: 'left', |
|
|
|
UpdateExpression: 'argument' |
|
|
|
}; |
|
|
|
import { blank, keys } from './utils/object.js'; |
|
|
|
import testForSideEffects from './utils/testForSideEffects.js'; |
|
|
|
|
|
|
|
export default class Declaration { |
|
|
|
constructor ( node ) { |
|
|
@ -22,6 +17,7 @@ export default class Declaration { |
|
|
|
this.name = null; |
|
|
|
|
|
|
|
this.isReassigned = false; |
|
|
|
this.mutations = []; |
|
|
|
this.aliases = []; |
|
|
|
} |
|
|
|
|
|
|
@ -34,54 +30,195 @@ export default class Declaration { |
|
|
|
this.name = reference.name; // TODO handle differences of opinion
|
|
|
|
|
|
|
|
if ( reference.isReassignment ) this.isReassigned = true; |
|
|
|
if ( reference.isMutation && !~this.mutations.indexOf( reference.statement ) ) { |
|
|
|
this.mutations.push( reference.statement ); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
mutates () { |
|
|
|
// returns a list of things this function mutates when it gets called
|
|
|
|
if ( !this._mutates ) { |
|
|
|
let mutatedNames = {}; |
|
|
|
hasSideEffect () { |
|
|
|
return testForSideEffects( this.functionBody, this.statement.scope, this.statement ); |
|
|
|
} |
|
|
|
|
|
|
|
const statement = this.statement; |
|
|
|
let scope = statement.scope; |
|
|
|
render ( es6 ) { |
|
|
|
if ( es6 ) return this.name; |
|
|
|
if ( !this.isReassigned || !this.isExported ) return this.name; |
|
|
|
|
|
|
|
const addNode = node => { |
|
|
|
while ( node.type === 'MemberExpression' ) node = node.object; |
|
|
|
if ( node.type === 'Identifier' ) mutatedNames[ node.name ] = true; |
|
|
|
}; |
|
|
|
return `exports.${this.name}`; |
|
|
|
} |
|
|
|
|
|
|
|
walk( this.functionBody, { |
|
|
|
enter ( node ) { |
|
|
|
if ( node._scope ) scope = node._scope; |
|
|
|
use () { |
|
|
|
this.isUsed = true; |
|
|
|
if ( this.statement ) this.statement.mark(); |
|
|
|
|
|
|
|
if ( node.type in modifierNodes ) { |
|
|
|
addNode( node[ modifierNodes[ node.type ] ] ); |
|
|
|
} else if ( node.type === 'CallExpression' ) { |
|
|
|
addNode( node.callee ); |
|
|
|
this.aliases.forEach( alias => alias.use() ); |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
export class SyntheticDefaultDeclaration { |
|
|
|
constructor ( node, statement, name ) { |
|
|
|
this.node = node; |
|
|
|
this.statement = statement; |
|
|
|
this.name = name; |
|
|
|
|
|
|
|
leave ( node ) { |
|
|
|
if ( node._scope ) scope = scope.parent; |
|
|
|
this.original = null; |
|
|
|
this.isExported = false; |
|
|
|
this.aliases = []; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
this._mutates = keys( mutatedNames ); |
|
|
|
addAlias ( declaration ) { |
|
|
|
this.aliases.push( declaration ); |
|
|
|
} |
|
|
|
|
|
|
|
return this._mutates; |
|
|
|
addReference ( reference ) { |
|
|
|
// Don't change the name to `default`; it's not a valid identifier name.
|
|
|
|
if ( reference.name === 'default' ) return; |
|
|
|
|
|
|
|
reference.declaration = this; |
|
|
|
this.name = reference.name; |
|
|
|
} |
|
|
|
|
|
|
|
render ( es6 ) { |
|
|
|
if ( es6 ) return this.name; |
|
|
|
if ( !this.isReassigned || !this.isExported ) return this.name; |
|
|
|
bind ( declaration ) { |
|
|
|
this.original = declaration; |
|
|
|
} |
|
|
|
|
|
|
|
return `exports.${this.name}`; |
|
|
|
hasSideEffect () { |
|
|
|
if ( this.original ) { |
|
|
|
return this.original.hasSideEffect(); |
|
|
|
} |
|
|
|
|
|
|
|
if ( /FunctionExpression/.test( this.node.declaration.type ) ) { |
|
|
|
return testForSideEffects( this.node.declaration.body, this.statement.scope, this.statement ); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
render () { |
|
|
|
return !this.original || this.original.isReassigned ? |
|
|
|
this.name : |
|
|
|
this.original.render(); |
|
|
|
} |
|
|
|
|
|
|
|
use () { |
|
|
|
this.isUsed = true; |
|
|
|
if ( this.statement ) this.statement.mark(); |
|
|
|
this.statement.mark(); |
|
|
|
|
|
|
|
if ( this.original ) this.original.use(); |
|
|
|
|
|
|
|
this.aliases.forEach( alias => alias.use() ); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
export class SyntheticNamespaceDeclaration { |
|
|
|
constructor ( module ) { |
|
|
|
this.module = module; |
|
|
|
this.name = null; |
|
|
|
|
|
|
|
this.needsNamespaceBlock = false; |
|
|
|
this.aliases = []; |
|
|
|
|
|
|
|
this.originals = blank(); |
|
|
|
module.getExports().forEach( name => { |
|
|
|
this.originals[ name ] = module.traceExport( name ); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
addAlias ( declaration ) { |
|
|
|
this.aliases.push( declaration ); |
|
|
|
} |
|
|
|
|
|
|
|
addReference ( reference ) { |
|
|
|
// if we have e.g. `foo.bar`, we can optimise
|
|
|
|
// the reference by pointing directly to `bar`
|
|
|
|
if ( reference.parts.length ) { |
|
|
|
reference.name = reference.parts.shift(); |
|
|
|
|
|
|
|
reference.end += reference.name.length + 1; // TODO this is brittle
|
|
|
|
|
|
|
|
const original = this.originals[ reference.name ]; |
|
|
|
|
|
|
|
// throw with an informative error message if the reference doesn't exist.
|
|
|
|
if ( !original ) { |
|
|
|
this.module.bundle.onwarn( `Export '${reference.name}' is not defined by '${this.module.id}'` ); |
|
|
|
reference.isUndefined = true; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
original.addReference( reference ); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// otherwise we're accessing the namespace directly,
|
|
|
|
// which means we need to mark all of this module's
|
|
|
|
// exports and render a namespace block in the bundle
|
|
|
|
if ( !this.needsNamespaceBlock ) { |
|
|
|
this.needsNamespaceBlock = true; |
|
|
|
this.module.bundle.internalNamespaces.push( this ); |
|
|
|
} |
|
|
|
|
|
|
|
reference.declaration = this; |
|
|
|
this.name = reference.name; |
|
|
|
} |
|
|
|
|
|
|
|
renderBlock ( indentString ) { |
|
|
|
const members = keys( this.originals ).map( name => { |
|
|
|
const original = this.originals[ name ]; |
|
|
|
|
|
|
|
if ( original.isReassigned ) { |
|
|
|
return `${indentString}get ${name} () { return ${original.render()}; }`; |
|
|
|
} |
|
|
|
|
|
|
|
return `${indentString}${name}: ${original.render()}`; |
|
|
|
}); |
|
|
|
|
|
|
|
return `var ${this.render()} = Object.freeze({\n${members.join( ',\n' )}\n});\n\n`; |
|
|
|
} |
|
|
|
|
|
|
|
render () { |
|
|
|
return this.name; |
|
|
|
} |
|
|
|
|
|
|
|
use () { |
|
|
|
keys( this.originals ).forEach( name => { |
|
|
|
this.originals[ name ].use(); |
|
|
|
}); |
|
|
|
|
|
|
|
this.aliases.forEach( alias => alias.use() ); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
export class ExternalDeclaration { |
|
|
|
constructor ( module, name ) { |
|
|
|
this.module = module; |
|
|
|
this.name = name; |
|
|
|
this.isExternal = true; |
|
|
|
} |
|
|
|
|
|
|
|
addAlias () { |
|
|
|
// noop
|
|
|
|
} |
|
|
|
|
|
|
|
addReference ( reference ) { |
|
|
|
reference.declaration = this; |
|
|
|
|
|
|
|
if ( this.name === 'default' || this.name === '*' ) { |
|
|
|
this.module.suggestName( reference.name ); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
render ( es6 ) { |
|
|
|
if ( this.name === '*' ) { |
|
|
|
return this.module.name; |
|
|
|
} |
|
|
|
|
|
|
|
if ( this.name === 'default' ) { |
|
|
|
return !es6 && this.module.exportsNames ? |
|
|
|
`${this.module.name}__default` : |
|
|
|
this.module.name; |
|
|
|
} |
|
|
|
|
|
|
|
return es6 ? this.name : `${this.module.name}.${this.name}`; |
|
|
|
} |
|
|
|
|
|
|
|
use () { |
|
|
|
// noop?
|
|
|
|
} |
|
|
|
} |
|
|
|