1 | import { basename, dirname, extname, relative, resolve } from 'path';
|
2 | import { readFile, Promise } from 'sander';
|
3 | import MagicString from 'magic-string';
|
4 | import { blank, keys } from './utils/object';
|
5 | import Module from './Module';
|
6 | import ExternalModule from './ExternalModule';
|
7 | import finalisers from './finalisers/index';
|
8 | import makeLegalIdentifier from './utils/makeLegalIdentifier';
|
9 | import ensureArray from './utils/ensureArray';
|
10 | import { defaultResolver, defaultExternalResolver } from './utils/resolvePath';
|
11 | import { defaultLoader } from './utils/load';
|
12 | import getExportMode from './utils/getExportMode';
|
13 | import getIndentString from './utils/getIndentString';
|
14 | import { unixizePath } from './utils/normalizePlatform.js';
|
15 |
|
16 | function badExports ( option, keys ) {
|
17 | throw new Error( `'${option}' was specified for options.exports, but entry module has following exports: ${keys.join(', ')}` );
|
18 | }
|
19 |
|
20 | export default class Bundle {
|
21 | constructor ( options ) {
|
22 | this.entryPath = resolve( options.entry ).replace( /\.js$/, '' ) + '.js';
|
23 | this.base = dirname( this.entryPath );
|
24 |
|
25 | this.resolvePath = options.resolvePath || defaultResolver;
|
26 | this.load = options.load || defaultLoader;
|
27 |
|
28 | this.resolvePathOptions = {
|
29 | external: ensureArray( options.external ),
|
30 | resolveExternal: options.resolveExternal || defaultExternalResolver
|
31 | };
|
32 |
|
33 | this.loadOptions = {
|
34 | transform: ensureArray( options.transform )
|
35 | };
|
36 |
|
37 | this.entryModule = null;
|
38 |
|
39 | this.varExports = blank();
|
40 | this.toExport = null;
|
41 |
|
42 | this.modulePromises = blank();
|
43 | this.statements = [];
|
44 | this.externalModules = [];
|
45 | this.internalNamespaceModules = [];
|
46 | this.assumedGlobals = blank();
|
47 | }
|
48 |
|
49 | fetchModule ( importee, importer ) {
|
50 | return Promise.resolve( importer === null ? importee : this.resolvePath( importee, importer, this.resolvePathOptions ) )
|
51 | .then( path => {
|
52 | if ( !path ) {
|
53 |
|
54 | if ( !this.modulePromises[ importee ] ) {
|
55 | const module = new ExternalModule( importee );
|
56 | this.externalModules.push( module );
|
57 | this.modulePromises[ importee ] = Promise.resolve( module );
|
58 | }
|
59 |
|
60 | return this.modulePromises[ importee ];
|
61 | }
|
62 |
|
63 | if ( !this.modulePromises[ path ] ) {
|
64 | this.modulePromises[ path ] = Promise.resolve( this.load( path, this.loadOptions ) )
|
65 | .then( source => {
|
66 | const module = new Module({
|
67 | path,
|
68 | source,
|
69 | bundle: this
|
70 | });
|
71 |
|
72 | return module;
|
73 | });
|
74 | }
|
75 |
|
76 | return this.modulePromises[ path ];
|
77 | });
|
78 | }
|
79 |
|
80 | build () {
|
81 |
|
82 | return this.fetchModule( this.entryPath, null )
|
83 | .then( entryModule => {
|
84 | const defaultExport = entryModule.exports.default;
|
85 |
|
86 | this.entryModule = entryModule;
|
87 |
|
88 | if ( defaultExport ) {
|
89 |
|
90 |
|
91 | if ( defaultExport.declaredName ) {
|
92 | entryModule.suggestName( 'default', defaultExport.declaredName );
|
93 | }
|
94 |
|
95 |
|
96 |
|
97 | else {
|
98 | let defaultExportName = makeLegalIdentifier( basename( this.entryPath ).slice( 0, -extname( this.entryPath ).length ) );
|
99 |
|
100 |
|
101 | let topLevelNames = [];
|
102 | entryModule.statements.forEach( statement => {
|
103 | keys( statement.defines ).forEach( name => topLevelNames.push( name ) );
|
104 | });
|
105 |
|
106 | while ( ~topLevelNames.indexOf( defaultExportName ) ) {
|
107 | defaultExportName = `_${defaultExportName}`;
|
108 | }
|
109 |
|
110 | entryModule.suggestName( 'default', defaultExportName );
|
111 | }
|
112 | }
|
113 |
|
114 | return entryModule.expandAllStatements( true );
|
115 | })
|
116 | .then( statements => {
|
117 | this.statements = statements;
|
118 | this.deconflict();
|
119 | });
|
120 | }
|
121 |
|
122 | deconflict () {
|
123 | let definers = blank();
|
124 | let conflicts = blank();
|
125 |
|
126 |
|
127 | this.statements.forEach( statement => {
|
128 | const module = statement.module;
|
129 | const names = keys( statement.defines );
|
130 |
|
131 |
|
132 |
|
133 |
|
134 | if ( statement.node.type === 'ExportDefaultDeclaration' ) {
|
135 | const name = module.getCanonicalName( 'default' );
|
136 |
|
137 | const isProxy = statement.node.declaration && statement.node.declaration.type === 'Identifier';
|
138 | const shouldDeconflict = !isProxy || ( module.getCanonicalName( statement.node.declaration.name ) !== name );
|
139 |
|
140 | if ( shouldDeconflict && !~names.indexOf( name ) ) {
|
141 | names.push( name );
|
142 | }
|
143 | }
|
144 |
|
145 | names.forEach( name => {
|
146 | if ( definers[ name ] ) {
|
147 | conflicts[ name ] = true;
|
148 | } else {
|
149 | definers[ name ] = [];
|
150 | }
|
151 |
|
152 |
|
153 |
|
154 | definers[ name ].push( module );
|
155 | });
|
156 | });
|
157 |
|
158 |
|
159 | this.externalModules.forEach( module => {
|
160 |
|
161 | let name = makeLegalIdentifier( module.suggestedNames['*'] || module.suggestedNames.default || module.id );
|
162 |
|
163 | if ( definers[ name ] ) {
|
164 | conflicts[ name ] = true;
|
165 | } else {
|
166 | definers[ name ] = [];
|
167 | }
|
168 |
|
169 | definers[ name ].push( module );
|
170 | module.name = name;
|
171 | });
|
172 |
|
173 |
|
174 | keys( this.assumedGlobals ).forEach( name => {
|
175 | if ( definers[ name ] ) {
|
176 | conflicts[ name ] = true;
|
177 | }
|
178 | });
|
179 |
|
180 |
|
181 | keys( conflicts ).forEach( name => {
|
182 | const modules = definers[ name ];
|
183 |
|
184 | if ( !this.assumedGlobals[ name ] ) {
|
185 |
|
186 |
|
187 |
|
188 | modules.pop();
|
189 | }
|
190 |
|
191 | modules.forEach( module => {
|
192 | const replacement = getSafeName( name );
|
193 | module.rename( name, replacement );
|
194 | });
|
195 | });
|
196 |
|
197 | function getSafeName ( name ) {
|
198 | while ( conflicts[ name ] ) {
|
199 | name = `_${name}`;
|
200 | }
|
201 |
|
202 | conflicts[ name ] = true;
|
203 | return name;
|
204 | }
|
205 | }
|
206 |
|
207 | generate ( options = {} ) {
|
208 | let magicString = new MagicString.Bundle({ separator: '' });
|
209 |
|
210 | const format = options.format || 'es6';
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 | let allBundleExports = blank();
|
228 |
|
229 | if ( format !== 'es6' ) {
|
230 | keys( this.entryModule.exports ).forEach( key => {
|
231 | const exportDeclaration = this.entryModule.exports[ key ];
|
232 |
|
233 | const originalDeclaration = this.entryModule.findDeclaration( exportDeclaration.localName );
|
234 |
|
235 | if ( originalDeclaration && originalDeclaration.type === 'VariableDeclaration' ) {
|
236 | const canonicalName = this.entryModule.getCanonicalName( exportDeclaration.localName );
|
237 |
|
238 | allBundleExports[ canonicalName ] = `exports.${key}`;
|
239 | this.varExports[ key ] = true;
|
240 | }
|
241 | });
|
242 | }
|
243 |
|
244 |
|
245 |
|
246 | this.toExport = keys( this.entryModule.exports )
|
247 | .filter( key => !this.varExports[ key ] );
|
248 |
|
249 |
|
250 | let previousModule = null;
|
251 | let previousIndex = -1;
|
252 | let previousMargin = 0;
|
253 |
|
254 | this.statements.forEach( statement => {
|
255 |
|
256 | if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) {
|
257 | return;
|
258 | }
|
259 |
|
260 | let replacements = blank();
|
261 | let bundleExports = blank();
|
262 |
|
263 | keys( statement.dependsOn )
|
264 | .concat( keys( statement.defines ) )
|
265 | .forEach( name => {
|
266 | const canonicalName = statement.module.getCanonicalName( name );
|
267 |
|
268 | if ( allBundleExports[ canonicalName ] ) {
|
269 | bundleExports[ name ] = replacements[ name ] = allBundleExports[ canonicalName ];
|
270 | } else if ( name !== canonicalName ) {
|
271 | replacements[ name ] = canonicalName;
|
272 | }
|
273 | });
|
274 |
|
275 | const source = statement.replaceIdentifiers( replacements, bundleExports );
|
276 |
|
277 |
|
278 | if ( statement.isExportDeclaration ) {
|
279 |
|
280 | if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.declaration.type === 'VariableDeclaration' ) {
|
281 | source.remove( statement.node.start, statement.node.declaration.start );
|
282 | }
|
283 |
|
284 |
|
285 |
|
286 | else if ( statement.node.declaration.id ) {
|
287 | source.remove( statement.node.start, statement.node.declaration.start );
|
288 | }
|
289 |
|
290 | else if ( statement.node.type === 'ExportDefaultDeclaration' ) {
|
291 | const module = statement.module;
|
292 | const canonicalName = module.getCanonicalName( 'default' );
|
293 |
|
294 | if ( statement.node.declaration.type === 'Identifier' && canonicalName === module.getCanonicalName( statement.node.declaration.name ) ) {
|
295 | return;
|
296 | }
|
297 |
|
298 | source.overwrite( statement.node.start, statement.node.declaration.start, `var ${canonicalName} = ` );
|
299 | }
|
300 |
|
301 | else {
|
302 | throw new Error( 'Unhandled export' );
|
303 | }
|
304 | }
|
305 |
|
306 |
|
307 |
|
308 | const minSeparation = ( previousModule !== statement.module ) || ( statement.index !== previousIndex + 1 ) ? 3 : 2;
|
309 | const margin = Math.max( minSeparation, statement.margin[0], previousMargin );
|
310 | let newLines = new Array( margin ).join( '\n' );
|
311 |
|
312 |
|
313 | if ( statement.leadingComments.length ) {
|
314 | const commentBlock = newLines + statement.leadingComments.map( ({ separator, comment }) => {
|
315 | return separator + ( comment.block ?
|
316 | `/*${comment.text}*/` :
|
317 | `//${comment.text}` );
|
318 | }).join( '' );
|
319 |
|
320 | magicString.addSource( new MagicString( commentBlock ) );
|
321 | newLines = new Array( statement.margin[0] ).join( '\n' );
|
322 | }
|
323 |
|
324 |
|
325 | magicString.addSource({
|
326 | content: source,
|
327 | separator: newLines
|
328 | });
|
329 |
|
330 |
|
331 | const comment = statement.trailingComment;
|
332 | if ( comment ) {
|
333 | const commentBlock = comment.block ?
|
334 | ` /*${comment.text}*/` :
|
335 | ` //${comment.text}`;
|
336 |
|
337 | magicString.append( commentBlock );
|
338 | }
|
339 |
|
340 | previousMargin = statement.margin[1];
|
341 | previousModule = statement.module;
|
342 | previousIndex = statement.index;
|
343 | });
|
344 |
|
345 |
|
346 | const indentString = magicString.getIndentString();
|
347 | const namespaceBlock = this.internalNamespaceModules.map( module => {
|
348 | const exportKeys = keys( module.exports );
|
349 |
|
350 | return `var ${module.getCanonicalName('*')} = {\n` +
|
351 | exportKeys.map( key => `${indentString}get ${key} () { return ${module.getCanonicalName(key)}; }` ).join( ',\n' ) +
|
352 | `\n};\n\n`;
|
353 | }).join( '' );
|
354 |
|
355 | magicString.prepend( namespaceBlock );
|
356 |
|
357 | const finalise = finalisers[ format ];
|
358 |
|
359 | if ( !finalise ) {
|
360 | throw new Error( `You must specify an output type - valid options are ${keys( finalisers ).join( ', ' )}` );
|
361 | }
|
362 |
|
363 | magicString = finalise( this, magicString.trim(), {
|
364 |
|
365 | exportMode: getExportMode( this, options.exports ),
|
366 |
|
367 |
|
368 | indentString: getIndentString( magicString, options )
|
369 | }, options );
|
370 |
|
371 | const code = magicString.toString();
|
372 | let map = null;
|
373 |
|
374 | if ( options.sourceMap ) {
|
375 | map = magicString.generateMap({
|
376 | includeContent: true,
|
377 | file: options.sourceMapFile || options.dest
|
378 |
|
379 | });
|
380 |
|
381 |
|
382 | const dir = dirname( map.file );
|
383 | map.sources = map.sources.map( source => {
|
384 | return source ? unixizePath( relative( dir, source ) ) : null
|
385 | });
|
386 | }
|
387 |
|
388 | return { code, map };
|
389 | }
|
390 | }
|