UNPKG

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