UNPKG

15.7 kBJavaScriptView Raw
1import { Promise } from 'sander';
2import MagicString from 'magic-string';
3import { blank, keys } from './utils/object';
4import Module from './Module';
5import ExternalModule from './ExternalModule';
6import finalisers from './finalisers/index';
7import makeLegalIdentifier from './utils/makeLegalIdentifier';
8import ensureArray from './utils/ensureArray';
9import { defaultResolver, defaultExternalResolver } from './utils/resolveId';
10import { defaultLoader } from './utils/load';
11import getExportMode from './utils/getExportMode';
12import getIndentString from './utils/getIndentString';
13import { unixizePath } from './utils/normalizePlatform.js';
14
15export default class Bundle {
16 constructor ( options ) {
17 this.entry = options.entry;
18 this.entryModule = null;
19
20 this.resolveId = options.resolveId || defaultResolver;
21 this.load = options.load || defaultLoader;
22
23 this.resolveOptions = {
24 external: ensureArray( options.external ),
25 resolveExternal: options.resolveExternal || defaultExternalResolver
26 };
27
28 this.loadOptions = {
29 transform: ensureArray( options.transform )
30 };
31
32 this.toExport = null;
33
34 this.pending = blank();
35 this.moduleById = blank();
36 this.modules = [];
37
38 this.statements = null;
39 this.externalModules = [];
40 this.internalNamespaceModules = [];
41
42 this.assumedGlobals = blank();
43 this.assumedGlobals.exports = true; // TODO strictly speaking, this only applies with non-ES6, non-default-only bundles
44 }
45
46 build () {
47 return Promise.resolve( this.resolveId( this.entry, undefined, this.resolveOptions ) )
48 .then( id => this.fetchModule( id ) )
49 .then( entryModule => {
50 entryModule.bindImportSpecifiers();
51
52 const defaultExport = entryModule.exports.default;
53
54 this.entryModule = entryModule;
55
56 if ( defaultExport ) {
57 entryModule.needsDefault = true;
58
59 // `export default function foo () {...}` -
60 // use the declared name for the export
61 if ( defaultExport.identifier ) {
62 entryModule.suggestName( 'default', defaultExport.identifier );
63 }
64
65 // `export default a + b` - generate an export name
66 // based on the id of the entry module
67 else {
68 let defaultExportName = this.entryModule.basename();
69
70 // deconflict
71 let topLevelNames = [];
72 entryModule.statements.forEach( statement => {
73 keys( statement.defines ).forEach( name => topLevelNames.push( name ) );
74 });
75
76 while ( ~topLevelNames.indexOf( defaultExportName ) ) {
77 defaultExportName = `_${defaultExportName}`;
78 }
79
80 entryModule.suggestName( 'default', defaultExportName );
81 }
82 }
83
84 entryModule.markAllStatements( true );
85 this.markAllModifierStatements();
86
87 // Include all side-effects
88 // TODO does this obviate the need for markAllStatements throughout?
89 this.modules.forEach( module => {
90 module.markAllSideEffects();
91 });
92
93 this.orderedModules = this.sort();
94 });
95 }
96
97 // TODO would be better to deconflict once, rather than per-render
98 deconflict ( es6 ) {
99 let nameCount = blank();
100
101 // ensure no conflicts with globals
102 keys( this.assumedGlobals ).forEach( name => nameCount[ name ] = 0 );
103
104 let allReplacements = blank();
105
106 // Assign names to external modules
107 this.externalModules.forEach( module => {
108 // while we're here...
109 allReplacements[ module.id ] = blank();
110
111 // TODO is this necessary in the ES6 case?
112 let name = makeLegalIdentifier( module.suggestedNames['*'] || module.suggestedNames.default || module.id );
113 module.name = getSafeName( name );
114 });
115
116 // Discover conflicts (i.e. two statements in separate modules both define `foo`)
117 let i = this.orderedModules.length;
118 while ( i-- ) {
119 const module = this.orderedModules[i];
120
121 // while we're here...
122 allReplacements[ module.id ] = blank();
123
124 keys( module.definitions ).forEach( name => {
125 const safeName = getSafeName( name );
126 if ( safeName !== name ) {
127 module.rename( name, safeName );
128 allReplacements[ module.id ][ name ] = safeName;
129 }
130 });
131 }
132
133 // Assign non-conflicting names to internal default/namespace export
134 this.orderedModules.forEach( module => {
135 if ( !module.needsDefault && !module.needsAll ) return;
136
137 if ( module.needsAll ) {
138 const namespaceName = getSafeName( module.suggestedNames[ '*' ] );
139 module.replacements[ '*' ] = namespaceName;
140 }
141
142 if ( module.needsDefault || module.needsAll && module.exports.default ) {
143 const defaultExport = module.exports.default;
144
145 // only create a new name if either
146 // a) it's an expression (`export default 42`) or
147 // b) it's a name that is reassigned to (`export var a = 1; a = 2`)
148 if ( defaultExport && defaultExport.identifier && !defaultExport.isModified ) return; // TODO encapsulate check for whether we need synthetic default name
149
150 const defaultName = getSafeName( module.suggestedNames.default );
151 module.replacements.default = defaultName;
152 }
153 });
154
155 this.orderedModules.forEach( module => {
156 keys( module.imports ).forEach( localName => {
157 if ( !module.imports[ localName ].isUsed ) return;
158
159 const bundleName = this.trace( module, localName, es6 );
160 if ( bundleName !== localName ) {
161 allReplacements[ module.id ][ localName ] = bundleName;
162 }
163 });
164 });
165
166 function getSafeName ( name ) {
167 if ( name in nameCount ) {
168 nameCount[ name ] += 1;
169 name = `${name}$${nameCount[ name ]}`;
170
171 while ( name in nameCount ) name = `_${name}`; // just to avoid any crazy edge cases
172 return name;
173 }
174
175 nameCount[ name ] = 0;
176 return name;
177 }
178
179 return allReplacements;
180 }
181
182 fetchModule ( id ) {
183 // short-circuit cycles
184 if ( this.pending[ id ] ) return null;
185 this.pending[ id ] = true;
186
187 return Promise.resolve( this.load( id, this.loadOptions ) )
188 .then( source => {
189 let ast;
190
191 if ( typeof source === 'object' ) {
192 ast = source.ast;
193 source = source.code;
194 }
195
196 const module = new Module({
197 id,
198 source,
199 ast,
200 bundle: this
201 });
202
203 this.modules.push( module );
204 this.moduleById[ id ] = module;
205
206 return this.fetchAllDependencies( module ).then( () => module );
207 });
208 }
209
210 fetchAllDependencies ( module ) {
211 const promises = module.dependencies.map( source => {
212 return Promise.resolve( this.resolveId( source, module.id, this.resolveOptions ) )
213 .then( resolvedId => {
214 module.resolvedIds[ source ] = resolvedId || source;
215
216 // external module
217 if ( !resolvedId ) {
218 if ( !this.moduleById[ source ] ) {
219 const module = new ExternalModule( source );
220 this.externalModules.push( module );
221 this.moduleById[ source ] = module;
222 }
223 }
224
225 else if ( resolvedId === module.id ) {
226 throw new Error( `A module cannot import itself (${resolvedId})` );
227 }
228
229 else {
230 return this.fetchModule( resolvedId );
231 }
232 });
233 });
234
235 return Promise.all( promises );
236 }
237
238 markAllModifierStatements () {
239 let settled = true;
240
241 this.modules.forEach( module => {
242 module.statements.forEach( statement => {
243 if ( statement.isIncluded ) return;
244
245 keys( statement.modifies ).forEach( name => {
246 const definingStatement = module.definitions[ name ];
247 const exportDeclaration = module.exports[ name ] || module.reexports[ name ] || (
248 module.exports.default && module.exports.default.identifier === name && module.exports.default
249 );
250
251 const shouldMark = ( definingStatement && definingStatement.isIncluded ) ||
252 ( exportDeclaration && exportDeclaration.isUsed );
253
254 if ( shouldMark ) {
255 settled = false;
256 statement.mark();
257 return;
258 }
259
260 // special case - https://github.com/rollup/rollup/pull/40
261 // TODO refactor this? it's a bit confusing
262 const importDeclaration = module.imports[ name ];
263 if ( !importDeclaration || importDeclaration.module.isExternal ) return;
264
265 if ( importDeclaration.name === '*' ) {
266 importDeclaration.module.markAllExportStatements();
267 } else {
268 const otherExportDeclaration = importDeclaration.module.exports[ importDeclaration.name ];
269 // TODO things like `export default a + b` don't apply here... right?
270 const otherDefiningStatement = module.findDefiningStatement( otherExportDeclaration.localName );
271
272 if ( !otherDefiningStatement ) return;
273
274 statement.mark();
275 }
276
277 settled = false;
278 });
279 });
280 });
281
282 if ( !settled ) this.markAllModifierStatements();
283 }
284
285 render ( options = {} ) {
286 const format = options.format || 'es6';
287 const allReplacements = this.deconflict( format === 'es6' );
288
289 // Determine export mode - 'default', 'named', 'none'
290 const exportMode = getExportMode( this, options.exports );
291
292 // If we have named exports from the bundle, and those exports
293 // are assigned to *within* the bundle, we may need to rewrite e.g.
294 //
295 // export let count = 0;
296 // export function incr () { count++ }
297 //
298 // might become...
299 //
300 // exports.count = 0;
301 // function incr () {
302 // exports.count += 1;
303 // }
304 // exports.incr = incr;
305 //
306 // This doesn't apply if the bundle is exported as ES6!
307 let allBundleExports = blank();
308 let isReassignedVarDeclaration = blank();
309 let varExports = blank();
310 let getterExports = [];
311
312 this.orderedModules.forEach( module => {
313 module.reassignments.forEach( name => {
314 isReassignedVarDeclaration[ module.replacements[ name ] || name ] = true;
315 });
316 });
317
318 if ( format !== 'es6' && exportMode === 'named' ) {
319 keys( this.entryModule.exports )
320 .concat( keys( this.entryModule.reexports ) )
321 .forEach( name => {
322 const canonicalName = this.traceExport( this.entryModule, name );
323
324 if ( isReassignedVarDeclaration[ canonicalName ] ) {
325 varExports[ name ] = true;
326
327 // if the same binding is exported multiple ways, we need to
328 // use getters to keep all exports in sync
329 if ( allBundleExports[ canonicalName ] ) {
330 getterExports.push({ key: name, value: allBundleExports[ canonicalName ] });
331 } else {
332 allBundleExports[ canonicalName ] = `exports.${name}`;
333 }
334 }
335 });
336 }
337
338 // since we're rewriting variable exports, we want to
339 // ensure we don't try and export them again at the bottom
340 this.toExport = this.entryModule.getExports()
341 .filter( key => !varExports[ key ] );
342
343 let magicString = new MagicString.Bundle({ separator: '\n\n' });
344
345 this.orderedModules.forEach( module => {
346 const source = module.render( allBundleExports, allReplacements[ module.id ], format );
347 if ( source.toString().length ) {
348 magicString.addSource( source );
349 }
350 });
351
352 // prepend bundle with internal namespaces
353 const indentString = getIndentString( magicString, options );
354 const namespaceBlock = this.internalNamespaceModules.map( module => {
355 const exports = keys( module.exports )
356 .concat( keys( module.reexports ) )
357 .map( name => {
358 const canonicalName = this.traceExport( module, name );
359 return `${indentString}get ${name} () { return ${canonicalName}; }`;
360 });
361
362 return `var ${module.replacements['*']} = {\n` +
363 exports.join( ',\n' ) +
364 `\n};\n\n`;
365 }).join( '' );
366
367 magicString.prepend( namespaceBlock );
368
369 if ( getterExports.length ) {
370 // TODO offer ES3-safe (but not spec-compliant) alternative?
371 const getterExportsBlock = `Object.defineProperties(exports, {\n` +
372 getterExports.map( ({ key, value }) => indentString + `${key}: { get: function () { return ${value}; } }` ).join( ',\n' ) +
373 `\n});`;
374
375 magicString.append( '\n\n' + getterExportsBlock );
376 }
377
378 const finalise = finalisers[ format ];
379
380 if ( !finalise ) {
381 throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` );
382 }
383
384 magicString = finalise( this, magicString.trim(), { exportMode, indentString }, options );
385
386 if ( options.banner ) magicString.prepend( options.banner + '\n' );
387 if ( options.footer ) magicString.append( '\n' + options.footer );
388
389 const code = magicString.toString();
390 let map = null;
391
392 if ( options.sourceMap ) {
393 const file = options.sourceMapFile || options.dest;
394 map = magicString.generateMap({
395 includeContent: true,
396 file
397 // TODO
398 });
399
400 map.sources = map.sources.map( unixizePath );
401 }
402
403 return { code, map };
404 }
405
406 sort () {
407 let seen = {};
408 let ordered = [];
409 let hasCycles;
410
411 let strongDeps = {};
412 let stronglyDependsOn = {};
413
414 function visit ( module ) {
415 seen[ module.id ] = true;
416
417 const { strongDependencies, weakDependencies } = module.consolidateDependencies();
418
419 strongDeps[ module.id ] = [];
420 stronglyDependsOn[ module.id ] = {};
421
422 keys( strongDependencies ).forEach( id => {
423 const imported = strongDependencies[ id ];
424
425 strongDeps[ module.id ].push( imported );
426
427 if ( seen[ id ] ) {
428 // we need to prevent an infinite loop, and note that
429 // we need to check for strong/weak dependency relationships
430 hasCycles = true;
431 return;
432 }
433
434 visit( imported );
435 });
436
437 keys( weakDependencies ).forEach( id => {
438 const imported = weakDependencies[ id ];
439
440 if ( seen[ id ] ) {
441 // we need to prevent an infinite loop, and note that
442 // we need to check for strong/weak dependency relationships
443 hasCycles = true;
444 return;
445 }
446
447 visit( imported );
448 });
449
450 // add second (and third...) order dependencies
451 function addStrongDependencies ( dependency ) {
452 if ( stronglyDependsOn[ module.id ][ dependency.id ] ) return;
453
454 stronglyDependsOn[ module.id ][ dependency.id ] = true;
455 strongDeps[ dependency.id ].forEach( addStrongDependencies );
456 }
457
458 strongDeps[ module.id ].forEach( addStrongDependencies );
459
460 ordered.push( module );
461 }
462
463 visit( this.entryModule );
464
465 if ( hasCycles ) {
466 let unordered = ordered;
467 ordered = [];
468
469 // unordered is actually semi-ordered, as [ fewer dependencies ... more dependencies ]
470 unordered.forEach( module => {
471 // ensure strong dependencies of `module` that don't strongly depend on `module` go first
472 strongDeps[ module.id ].forEach( place );
473
474 function place ( dep ) {
475 if ( !stronglyDependsOn[ dep.id ][ module.id ] && !~ordered.indexOf( dep ) ) {
476 strongDeps[ dep.id ].forEach( place );
477 ordered.push( dep );
478 }
479 }
480
481 if ( !~ordered.indexOf( module ) ) {
482 ordered.push( module );
483 }
484 });
485 }
486
487 return ordered;
488 }
489
490 trace ( module, localName, es6 ) {
491 const importDeclaration = module.imports[ localName ];
492
493 // defined in this module
494 if ( !importDeclaration ) return module.replacements[ localName ] || localName;
495
496 // defined elsewhere
497 return this.traceExport( importDeclaration.module, importDeclaration.name, es6 );
498 }
499
500 traceExport ( module, name, es6 ) {
501 if ( module.isExternal ) {
502 if ( name === 'default' ) return module.needsNamed && !es6 ? `${module.name}__default` : module.name;
503 if ( name === '*' ) return module.name;
504 return es6 ? name : `${module.name}.${name}`;
505 }
506
507 const reexportDeclaration = module.reexports[ name ];
508 if ( reexportDeclaration ) {
509 return this.traceExport( reexportDeclaration.module, reexportDeclaration.localName );
510 }
511
512 if ( name === '*' ) return module.replacements[ '*' ];
513 if ( name === 'default' ) return module.defaultName();
514
515 const exportDeclaration = module.exports[ name ];
516 if ( exportDeclaration ) return this.trace( module, exportDeclaration.localName );
517
518 for ( let i = 0; i < module.exportAlls.length; i += 1 ) {
519 const declaration = module.exportAlls[i];
520 if ( declaration.module.exports[ name ] ) {
521 return this.traceExport( declaration.module, name, es6 );
522 }
523 }
524
525 throw new Error( `Could not trace binding '${name}' from ${module.id}` );
526 }
527}