UNPKG

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