1 | import { Promise } from 'sander';
|
2 | import MagicString from 'magic-string';
|
3 | import { blank, keys } from './utils/object';
|
4 | import Module from './Module';
|
5 | import ExternalModule from './ExternalModule';
|
6 | import finalisers from './finalisers/index';
|
7 | import makeLegalIdentifier from './utils/makeLegalIdentifier';
|
8 | import ensureArray from './utils/ensureArray';
|
9 | import { defaultResolver, defaultExternalResolver } from './utils/resolveId';
|
10 | import { defaultLoader } from './utils/load';
|
11 | import getExportMode from './utils/getExportMode';
|
12 | import getIndentString from './utils/getIndentString';
|
13 | import { unixizePath } from './utils/normalizePlatform.js';
|
14 |
|
15 | export 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;
|
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 |
|
60 |
|
61 | if ( defaultExport.identifier ) {
|
62 | entryModule.suggestName( 'default', defaultExport.identifier );
|
63 | }
|
64 |
|
65 |
|
66 |
|
67 | else {
|
68 | let defaultExportName = this.entryModule.basename();
|
69 |
|
70 |
|
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 |
|
88 |
|
89 | this.modules.forEach( module => {
|
90 | module.markAllSideEffects();
|
91 | });
|
92 |
|
93 | this.orderedModules = this.sort();
|
94 | });
|
95 | }
|
96 |
|
97 |
|
98 | deconflict ( es6 ) {
|
99 | let nameCount = blank();
|
100 |
|
101 |
|
102 | keys( this.assumedGlobals ).forEach( name => nameCount[ name ] = 0 );
|
103 |
|
104 | let allReplacements = blank();
|
105 |
|
106 |
|
107 | this.externalModules.forEach( module => {
|
108 |
|
109 | allReplacements[ module.id ] = blank();
|
110 |
|
111 |
|
112 | let name = makeLegalIdentifier( module.suggestedNames['*'] || module.suggestedNames.default || module.id );
|
113 | module.name = getSafeName( name );
|
114 | });
|
115 |
|
116 |
|
117 | let i = this.orderedModules.length;
|
118 | while ( i-- ) {
|
119 | const module = this.orderedModules[i];
|
120 |
|
121 |
|
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 |
|
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 |
|
146 |
|
147 |
|
148 | if ( defaultExport && defaultExport.identifier && !defaultExport.isModified ) return;
|
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}`;
|
172 | return name;
|
173 | }
|
174 |
|
175 | nameCount[ name ] = 0;
|
176 | return name;
|
177 | }
|
178 |
|
179 | return allReplacements;
|
180 | }
|
181 |
|
182 | fetchModule ( id ) {
|
183 |
|
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 |
|
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 |
|
261 |
|
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 |
|
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 |
|
290 | const exportMode = getExportMode( this, options.exports );
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 |
|
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 |
|
328 |
|
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 |
|
339 |
|
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 |
|
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 |
|
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 |
|
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 |
|
429 |
|
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 |
|
442 |
|
443 | hasCycles = true;
|
444 | return;
|
445 | }
|
446 |
|
447 | visit( imported );
|
448 | });
|
449 |
|
450 |
|
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 |
|
470 | unordered.forEach( module => {
|
471 |
|
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 |
|
494 | if ( !importDeclaration ) return module.replacements[ localName ] || localName;
|
495 |
|
496 |
|
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 | }
|