UNPKG

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