UNPKG

8.99 kBJavaScriptView Raw
1import Promise from 'es6-promise/lib/es6-promise/promise.js';
2import MagicString from 'magic-string';
3import first from './utils/first.js';
4import { blank, keys } from './utils/object.js';
5import Module from './Module.js';
6import ExternalModule from './ExternalModule.js';
7import finalisers from './finalisers/index.js';
8import ensureArray from './utils/ensureArray.js';
9import { load, onwarn, resolveId } from './utils/defaults.js';
10import getExportMode from './utils/getExportMode.js';
11import getIndentString from './utils/getIndentString.js';
12import { unixizePath } from './utils/normalizePlatform.js';
13import transform from './utils/transform.js';
14import collapseSourcemaps from './utils/collapseSourcemaps.js';
15import callIfFunction from './utils/callIfFunction.js';
16
17export default class Bundle {
18 constructor ( options ) {
19 this.entry = options.entry;
20 this.entryModule = null;
21
22 this.plugins = ensureArray( options.plugins );
23
24 this.resolveId = first(
25 this.plugins
26 .map( plugin => plugin.resolveId )
27 .filter( Boolean )
28 .concat( resolveId )
29 );
30
31 this.load = first(
32 this.plugins
33 .map( plugin => plugin.load )
34 .filter( Boolean )
35 .concat( load )
36 );
37
38 this.transformers = this.plugins
39 .map( plugin => plugin.transform )
40 .filter( Boolean );
41
42 this.pending = blank();
43 this.moduleById = blank();
44 this.modules = [];
45
46 this.externalModules = [];
47 this.internalNamespaces = [];
48
49 this.assumedGlobals = blank();
50
51 this.external = options.external || [];
52 this.onwarn = options.onwarn || onwarn;
53
54 // TODO strictly speaking, this only applies with non-ES6, non-default-only bundles
55 [ 'module', 'exports' ].forEach( global => this.assumedGlobals[ global ] = true );
56 }
57
58 build () {
59 return Promise.resolve( this.resolveId( this.entry, undefined ) )
60 .then( id => this.fetchModule( id, undefined ) )
61 .then( entryModule => {
62 this.entryModule = entryModule;
63
64 this.modules.forEach( module => module.bindImportSpecifiers() );
65 this.modules.forEach( module => module.bindAliases() );
66 this.modules.forEach( module => module.bindReferences() );
67
68 // mark all export statements
69 entryModule.getExports().forEach( name => {
70 const declaration = entryModule.traceExport( name );
71 declaration.isExported = true;
72
73 declaration.use();
74 });
75
76 let settled = false;
77 while ( !settled ) {
78 settled = true;
79
80 this.modules.forEach( module => {
81 if ( module.markAllSideEffects() ) settled = false;
82 });
83 }
84
85 this.orderedModules = this.sort();
86 this.deconflict();
87 });
88 }
89
90 deconflict () {
91 let used = blank();
92
93 // ensure no conflicts with globals
94 keys( this.assumedGlobals ).forEach( name => used[ name ] = 1 );
95
96 function getSafeName ( name ) {
97 while ( used[ name ] ) {
98 name += `$${used[name]++}`;
99 }
100
101 used[ name ] = 1;
102 return name;
103 }
104
105 this.externalModules.forEach( module => {
106 module.name = getSafeName( module.name );
107 });
108
109 this.modules.forEach( module => {
110 keys( module.declarations ).forEach( originalName => {
111 const declaration = module.declarations[ originalName ];
112
113 if ( originalName === 'default' ) {
114 if ( declaration.original && !declaration.original.isReassigned ) return;
115 }
116
117 declaration.name = getSafeName( declaration.name );
118 });
119 });
120 }
121
122 fetchModule ( id, importer ) {
123 // short-circuit cycles
124 if ( this.pending[ id ] ) return null;
125 this.pending[ id ] = true;
126
127 return Promise.resolve( this.load( id ) )
128 .catch( err => {
129 let msg = `Could not load ${id}`;
130 if ( importer ) msg += ` (imported by ${importer})`;
131
132 msg += `: ${err.message}`;
133 throw new Error( msg );
134 })
135 .then( source => transform( source, id, this.transformers ) )
136 .then( source => {
137 const { code, originalCode, ast, sourceMapChain } = source;
138
139 const module = new Module({ id, code, originalCode, ast, sourceMapChain, bundle: this });
140
141 this.modules.push( module );
142 this.moduleById[ id ] = module;
143
144 return this.fetchAllDependencies( module ).then( () => module );
145 });
146 }
147
148 fetchAllDependencies ( module ) {
149 const promises = module.dependencies.map( source => {
150 return Promise.resolve( this.resolveId( source, module.id ) )
151 .then( resolvedId => {
152 if ( !resolvedId ) {
153 if ( !~this.external.indexOf( source ) ) this.onwarn( `Treating '${source}' as external dependency` );
154 module.resolvedIds[ source ] = source;
155
156 if ( !this.moduleById[ source ] ) {
157 const module = new ExternalModule( source );
158 this.externalModules.push( module );
159 this.moduleById[ source ] = module;
160 }
161 }
162
163 else {
164 if ( resolvedId === module.id ) {
165 throw new Error( `A module cannot import itself (${resolvedId})` );
166 }
167
168 module.resolvedIds[ source ] = resolvedId;
169 return this.fetchModule( resolvedId, module.id );
170 }
171 });
172 });
173
174 return Promise.all( promises );
175 }
176
177 render ( options = {} ) {
178 const format = options.format || 'es6';
179
180 // Determine export mode - 'default', 'named', 'none'
181 const exportMode = getExportMode( this, options.exports );
182
183 let magicString = new MagicString.Bundle({ separator: '\n\n' });
184 let usedModules = [];
185
186 this.orderedModules.forEach( module => {
187 const source = module.render( format === 'es6' );
188 if ( source.toString().length ) {
189 magicString.addSource( source );
190 usedModules.push( module );
191 }
192 });
193
194 const intro = [ options.intro ]
195 .concat(
196 this.plugins.map( plugin => plugin.intro && plugin.intro() )
197 )
198 .filter( Boolean )
199 .join( '\n\n' );
200
201 if ( intro ) magicString.prepend( intro + '\n' );
202 if ( options.outro ) magicString.append( '\n' + options.outro );
203
204 const indentString = getIndentString( magicString, options );
205
206 const finalise = finalisers[ format ];
207 if ( !finalise ) throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` );
208
209 magicString = finalise( this, magicString.trim(), { exportMode, indentString }, options );
210
211 const banner = [ options.banner ]
212 .concat( this.plugins.map( plugin => plugin.banner ) )
213 .map( callIfFunction )
214 .filter( Boolean )
215 .join( '\n' );
216
217 const footer = [ options.footer ]
218 .concat( this.plugins.map( plugin => plugin.footer ) )
219 .map( callIfFunction )
220 .filter( Boolean )
221 .join( '\n' );
222
223 if ( banner ) magicString.prepend( banner + '\n' );
224 if ( footer ) magicString.append( '\n' + footer );
225
226 const code = magicString.toString();
227 let map = null;
228
229 if ( options.sourceMap ) {
230 const file = options.sourceMapFile || options.dest;
231 map = magicString.generateMap({
232 includeContent: true,
233 file
234 // TODO
235 });
236
237 if ( this.transformers.length ) map = collapseSourcemaps( map, usedModules );
238 map.sources = map.sources.map( unixizePath );
239 }
240
241 return { code, map };
242 }
243
244 sort () {
245 let seen = {};
246 let ordered = [];
247 let hasCycles;
248
249 let strongDeps = {};
250 let stronglyDependsOn = {};
251
252 function visit ( module ) {
253 if ( seen[ module.id ] ) return;
254 seen[ module.id ] = true;
255
256 const { strongDependencies, weakDependencies } = module.consolidateDependencies();
257
258 strongDeps[ module.id ] = [];
259 stronglyDependsOn[ module.id ] = {};
260
261 keys( strongDependencies ).forEach( id => {
262 const imported = strongDependencies[ id ];
263
264 strongDeps[ module.id ].push( imported );
265
266 if ( seen[ id ] ) {
267 // we need to prevent an infinite loop, and note that
268 // we need to check for strong/weak dependency relationships
269 hasCycles = true;
270 return;
271 }
272
273 visit( imported );
274 });
275
276 keys( weakDependencies ).forEach( id => {
277 const imported = weakDependencies[ id ];
278
279 if ( seen[ id ] ) {
280 // we need to prevent an infinite loop, and note that
281 // we need to check for strong/weak dependency relationships
282 hasCycles = true;
283 return;
284 }
285
286 visit( imported );
287 });
288
289 // add second (and third...) order dependencies
290 function addStrongDependencies ( dependency ) {
291 if ( stronglyDependsOn[ module.id ][ dependency.id ] ) return;
292
293 stronglyDependsOn[ module.id ][ dependency.id ] = true;
294 strongDeps[ dependency.id ].forEach( addStrongDependencies );
295 }
296
297 strongDeps[ module.id ].forEach( addStrongDependencies );
298
299 ordered.push( module );
300 }
301
302 this.modules.forEach( visit );
303
304 if ( hasCycles ) {
305 let unordered = ordered;
306 ordered = [];
307
308 // unordered is actually semi-ordered, as [ fewer dependencies ... more dependencies ]
309 unordered.forEach( module => {
310 // ensure strong dependencies of `module` that don't strongly depend on `module` go first
311 strongDeps[ module.id ].forEach( place );
312
313 function place ( dep ) {
314 if ( !stronglyDependsOn[ dep.id ][ module.id ] && !~ordered.indexOf( dep ) ) {
315 strongDeps[ dep.id ].forEach( place );
316 ordered.push( dep );
317 }
318 }
319
320 if ( !~ordered.indexOf( module ) ) {
321 ordered.push( module );
322 }
323 });
324 }
325
326 return ordered;
327 }
328}