UNPKG

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