UNPKG

22.1 kBJavaScriptView Raw
1import { parse } from 'acorn';
2import MagicString from 'magic-string';
3import Statement from './Statement';
4import walk from './ast/walk';
5import { blank, keys } from './utils/object';
6import { basename, extname } from './utils/path';
7import getLocation from './utils/getLocation';
8import makeLegalIdentifier from './utils/makeLegalIdentifier';
9import SOURCEMAPPING_URL from './utils/sourceMappingURL';
10
11function deconflict ( name, names ) {
12 while ( name in names ) {
13 name = `_${name}`;
14 }
15
16 return name;
17}
18
19function isEmptyExportedVarDeclaration ( node, allBundleExports, moduleReplacements ) {
20 if ( node.type !== 'VariableDeclaration' || node.declarations[0].init ) return false;
21
22 const name = node.declarations[0].id.name;
23 const canonicalName = moduleReplacements[ name ] || name;
24
25 return canonicalName in allBundleExports;
26}
27
28export default class Module {
29 constructor ({ id, source, ast, bundle }) {
30 this.source = source;
31
32 this.bundle = bundle;
33 this.id = id;
34
35 // By default, `id` is the filename. Custom resolvers and loaders
36 // can change that, but it makes sense to use it for the source filename
37 this.magicString = new MagicString( source, {
38 filename: id
39 });
40
41 // remove existing sourceMappingURL comments
42 const pattern = new RegExp( `\\/\\/#\\s+${SOURCEMAPPING_URL}=.+\\n?`, 'g' );
43 let match;
44 while ( match = pattern.exec( source ) ) {
45 this.magicString.remove( match.index, match.index + match[0].length );
46 }
47
48 this.suggestedNames = blank();
49 this.comments = [];
50
51 this.statements = this.parse( ast );
52
53 // all dependencies
54 this.dependencies = [];
55 this.resolvedIds = blank();
56 this.boundImportSpecifiers = false;
57
58 // imports and exports, indexed by local name
59 this.imports = blank();
60 this.exports = blank();
61 this.reexports = blank();
62 this.exportDelegates = blank();
63
64 this.exportAlls = [];
65
66 this.replacements = blank();
67
68 this.reassignments = [];
69
70 this.marked = blank();
71 this.definitions = blank();
72 this.definitionPromises = blank();
73 this.modifications = blank();
74
75 this.analyse();
76 }
77
78 addExport ( statement ) {
79 const node = statement.node;
80 const source = node.source && node.source.value;
81
82 // export { name } from './other'
83 if ( source ) {
84 if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source );
85
86 if ( node.type === 'ExportAllDeclaration' ) {
87 // Store `export * from '...'` statements in an array of delegates.
88 // When an unknown import is encountered, we see if one of them can satisfy it.
89 this.exportAlls.push({
90 statement,
91 source
92 });
93 }
94
95 else {
96 node.specifiers.forEach( specifier => {
97 this.reexports[ specifier.exported.name ] = {
98 source,
99 localName: specifier.local.name,
100 module: null // filled in later
101 };
102 });
103 }
104 }
105
106 // export default function foo () {}
107 // export default foo;
108 // export default 42;
109 else if ( node.type === 'ExportDefaultDeclaration' ) {
110 const isDeclaration = /Declaration$/.test( node.declaration.type );
111 const isAnonymous = /(?:Class|Function)Expression$/.test( node.declaration.type );
112
113 const identifier = isDeclaration ?
114 node.declaration.id.name :
115 node.declaration.type === 'Identifier' ?
116 node.declaration.name :
117 null;
118
119 this.exports.default = {
120 statement,
121 name: 'default',
122 localName: identifier || 'default',
123 identifier,
124 isDeclaration,
125 isAnonymous,
126 isModified: false // in case of `export default foo; foo = somethingElse`
127 };
128 }
129
130 // export { foo, bar, baz }
131 // export var foo = 42;
132 // export function foo () {}
133 else if ( node.type === 'ExportNamedDeclaration' ) {
134 if ( node.specifiers.length ) {
135 // export { foo, bar, baz }
136 node.specifiers.forEach( specifier => {
137 const localName = specifier.local.name;
138 const exportedName = specifier.exported.name;
139
140 this.exports[ exportedName ] = {
141 statement,
142 localName,
143 exportedName
144 };
145 });
146 }
147
148 else {
149 let declaration = node.declaration;
150
151 let name;
152
153 if ( declaration.type === 'VariableDeclaration' ) {
154 // export var foo = 42
155 name = declaration.declarations[0].id.name;
156 } else {
157 // export function foo () {}
158 name = declaration.id.name;
159 }
160
161 this.exports[ name ] = {
162 statement,
163 localName: name,
164 expression: declaration
165 };
166 }
167 }
168 }
169
170 addImport ( statement ) {
171 const node = statement.node;
172 const source = node.source.value;
173
174 if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source );
175
176 node.specifiers.forEach( specifier => {
177 const isDefault = specifier.type === 'ImportDefaultSpecifier';
178 const isNamespace = specifier.type === 'ImportNamespaceSpecifier';
179
180 const localName = specifier.local.name;
181 const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;
182
183 if ( this.imports[ localName ] ) {
184 const err = new Error( `Duplicated import '${localName}'` );
185 err.file = this.id;
186 err.loc = getLocation( this.source, specifier.start );
187 throw err;
188 }
189
190 this.imports[ localName ] = {
191 source,
192 name,
193 localName
194 };
195 });
196 }
197
198 analyse () {
199 // discover this module's imports and exports
200 this.statements.forEach( statement => {
201 if ( statement.isImportDeclaration ) this.addImport( statement );
202 else if ( statement.isExportDeclaration ) this.addExport( statement );
203
204 statement.analyse();
205
206 // consolidate names that are defined/modified in this module
207 keys( statement.defines ).forEach( name => {
208 this.definitions[ name ] = statement;
209 });
210
211 keys( statement.modifies ).forEach( name => {
212 ( this.modifications[ name ] || ( this.modifications[ name ] = [] ) ).push( statement );
213 });
214 });
215
216 // discover variables that are reassigned inside function
217 // bodies, so we can keep bindings live, e.g.
218 //
219 // export var count = 0;
220 // export function incr () { count += 1 }
221 let reassigned = blank();
222 this.statements.forEach( statement => {
223 keys( statement.reassigns ).forEach( name => {
224 reassigned[ name ] = true;
225 });
226 });
227
228 // if names are referenced that are neither defined nor imported
229 // in this module, we assume that they're globals
230 this.statements.forEach( statement => {
231 if ( statement.isReexportDeclaration ) return;
232
233 // while we're here, mark reassignments
234 statement.scope.varDeclarations.forEach( name => {
235 if ( reassigned[ name ] && !~this.reassignments.indexOf( name ) ) {
236 this.reassignments.push( name );
237 }
238 });
239
240 keys( statement.dependsOn ).forEach( name => {
241 if ( !this.definitions[ name ] && !this.imports[ name ] ) {
242 this.bundle.assumedGlobals[ name ] = true;
243 }
244 });
245 });
246 }
247
248 basename () {
249 return makeLegalIdentifier( basename( this.id ).slice( 0, -extname( this.id ).length ) );
250 }
251
252 bindImportSpecifiers () {
253 if ( this.boundImportSpecifiers ) return;
254 this.boundImportSpecifiers = true;
255
256 [ this.imports, this.reexports ].forEach( specifiers => {
257 keys( specifiers ).forEach( name => {
258 const specifier = specifiers[ name ];
259
260 if ( specifier.module ) return;
261
262 const id = this.resolvedIds[ specifier.source ];
263 specifier.module = this.bundle.moduleById[ id ];
264 });
265 });
266
267 this.exportAlls.forEach( delegate => {
268 const id = this.resolvedIds[ delegate.source ];
269 delegate.module = this.bundle.moduleById[ id ];
270 });
271
272 this.dependencies.forEach( source => {
273 const id = this.resolvedIds[ source ];
274 const module = this.bundle.moduleById[ id ];
275
276 if ( !module.isExternal ) module.bindImportSpecifiers();
277 });
278 }
279
280 consolidateDependencies () {
281 let strongDependencies = blank();
282
283 function addDependency ( dependencies, declaration ) {
284 if ( declaration && declaration.module && !declaration.module.isExternal ) {
285 dependencies[ declaration.module.id ] = declaration.module;
286 return true;
287 }
288 }
289
290 this.statements.forEach( statement => {
291 if ( statement.isImportDeclaration && !statement.node.specifiers.length ) {
292 // include module for its side-effects
293 const id = this.resolvedIds[ statement.node.source.value ];
294 const module = this.bundle.moduleById[ id ];
295
296 if ( !module.isExternal ) strongDependencies[ module.id ] = module;
297 }
298
299 else if ( statement.isReexportDeclaration ) {
300 if ( statement.node.specifiers ) {
301 statement.node.specifiers.forEach( specifier => {
302 let reexport;
303
304 let module = this;
305 let name = specifier.exported.name;
306 while ( !module.isExternal && module.reexports[ name ] && module.reexports[ name ].isUsed ) {
307 reexport = module.reexports[ name ];
308 module = reexport.module;
309 name = reexport.localName;
310 }
311
312 addDependency( strongDependencies, reexport );
313 });
314 }
315 }
316
317 else {
318 keys( statement.stronglyDependsOn ).forEach( name => {
319 if ( statement.defines[ name ] ) return;
320
321 addDependency( strongDependencies, this.exportDelegates[ name ] ) ||
322 addDependency( strongDependencies, this.imports[ name ] );
323 });
324 }
325 });
326
327 let weakDependencies = blank();
328
329 this.statements.forEach( statement => {
330 keys( statement.dependsOn ).forEach( name => {
331 if ( statement.defines[ name ] ) return;
332
333 addDependency( weakDependencies, this.exportDelegates[ name ] ) ||
334 addDependency( weakDependencies, this.imports[ name ] );
335 });
336 });
337
338 // special case – `export { ... } from './other'` in entry module
339 if ( this.exportAlls.length ) {
340 this.exportAlls.forEach( ({ source }) => {
341 const resolved = this.resolvedIds[ source ];
342 const otherModule = this.bundle.moduleById[ resolved ];
343
344 strongDependencies[ otherModule.id ] = otherModule;
345 });
346 }
347
348 return { strongDependencies, weakDependencies };
349 }
350
351 defaultName () {
352 const defaultExport = this.exports.default;
353
354 if ( !defaultExport ) return null;
355
356 const name = defaultExport.identifier && !defaultExport.isModified ?
357 defaultExport.identifier :
358 this.replacements.default;
359
360 return this.replacements[ name ] || name;
361 }
362
363 findDefiningStatement ( name ) {
364 if ( this.definitions[ name ] ) return this.definitions[ name ];
365
366 // TODO what about `default`/`*`?
367
368 const importDeclaration = this.imports[ name ];
369 if ( !importDeclaration ) return null;
370
371 return importDeclaration.module.findDefiningStatement( name );
372 }
373
374 getExports () {
375 let exports = blank();
376
377 keys( this.exports ).forEach( name => {
378 exports[ name ] = true;
379 });
380
381 keys( this.reexports ).forEach( name => {
382 exports[ name ] = true;
383 });
384
385 this.exportAlls.forEach( ({ module }) => {
386 module.getExports().forEach( name => {
387 if ( name !== 'default' ) exports[ name ] = true;
388 });
389 });
390
391 return keys( exports );
392 }
393
394 mark ( name ) {
395 // shortcut cycles
396 if ( this.marked[ name ] ) return;
397 this.marked[ name ] = true;
398
399 // The definition for this name is in a different module
400 if ( this.imports[ name ] ) {
401 const importDeclaration = this.imports[ name ];
402 importDeclaration.isUsed = true;
403
404 const module = importDeclaration.module;
405
406 // suggest names. TODO should this apply to non default/* imports?
407 if ( importDeclaration.name === 'default' ) {
408 // TODO this seems ropey
409 const localName = importDeclaration.localName;
410 let suggestion = this.suggestedNames[ localName ] || localName;
411
412 // special case - the module has its own import by this name
413 while ( !module.isExternal && module.imports[ suggestion ] ) {
414 suggestion = `_${suggestion}`;
415 }
416
417 module.suggestName( 'default', suggestion );
418 } else if ( importDeclaration.name === '*' ) {
419 const localName = importDeclaration.localName;
420 const suggestion = this.suggestedNames[ localName ] || localName;
421 module.suggestName( '*', suggestion );
422 module.suggestName( 'default', `${suggestion}__default` );
423 }
424
425 if ( importDeclaration.name === 'default' ) {
426 module.needsDefault = true;
427 } else if ( importDeclaration.name === '*' ) {
428 module.needsAll = true;
429 } else {
430 module.needsNamed = true;
431 }
432
433 if ( module.isExternal ) {
434 module.importedByBundle.push( importDeclaration );
435 }
436
437 else if ( importDeclaration.name === '*' ) {
438 // we need to create an internal namespace
439 if ( !~this.bundle.internalNamespaceModules.indexOf( module ) ) {
440 this.bundle.internalNamespaceModules.push( module );
441 }
442
443 module.markAllExportStatements();
444 }
445
446 else {
447 module.markExport( importDeclaration.name, name, this );
448 }
449 }
450
451 else {
452 const statement = name === 'default' ? this.exports.default.statement : this.definitions[ name ];
453 if ( statement ) statement.mark();
454 }
455 }
456
457 markAllSideEffects () {
458 this.statements.forEach( statement => {
459 statement.markSideEffect();
460 });
461 }
462
463 markAllStatements ( isEntryModule ) {
464 this.statements.forEach( statement => {
465 if ( statement.isIncluded ) return; // TODO can this happen? probably not...
466
467 // skip import declarations...
468 if ( statement.isImportDeclaration ) {
469 // ...unless they're empty, in which case assume we're importing them for the side-effects
470 // THIS IS NOT FOOLPROOF. Probably need /*rollup: include */ or similar
471 if ( !statement.node.specifiers.length ) {
472 const id = this.resolvedIds[ statement.node.source.value ];
473 const otherModule = this.bundle.moduleById[ id ];
474
475 if ( !otherModule.isExternal ) otherModule.markAllStatements();
476 }
477 }
478
479 // skip `export { foo, bar, baz }`...
480 else if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) {
481 // ...but ensure they are defined, if this is the entry module
482 if ( isEntryModule ) statement.mark();
483 }
484
485 // include everything else
486 else {
487 statement.mark();
488 }
489 });
490 }
491
492 markAllExportStatements () {
493 this.statements.forEach( statement => {
494 if ( statement.isExportDeclaration ) statement.mark();
495 });
496 }
497
498 markExport ( name, suggestedName, importer ) {
499 const reexport = this.reexports[ name ];
500 const exportDeclaration = this.exports[ name ];
501
502 if ( reexport ) {
503 reexport.isUsed = true;
504 reexport.module.markExport( reexport.localName, suggestedName, this );
505 }
506
507 else if ( exportDeclaration ) {
508 exportDeclaration.isUsed = true;
509 if ( name === 'default' ) {
510 this.needsDefault = true;
511 this.suggestName( 'default', suggestedName );
512 return exportDeclaration.statement.mark();
513 }
514
515 this.mark( exportDeclaration.localName );
516 }
517
518 else {
519 // See if there exists an export delegate that defines `name`.
520 let i;
521 for ( i = 0; i < this.exportAlls.length; i += 1 ) {
522 const declaration = this.exportAlls[i];
523
524 if ( declaration.module.exports[ name ] ) {
525 // It's found! This module exports `name` through declaration.
526 // It is however not imported into this scope.
527 this.exportDelegates[ name ] = declaration;
528 declaration.module.markExport( name );
529
530 declaration.statement.dependsOn[ name ] =
531 declaration.statement.stronglyDependsOn[ name ] = true;
532
533 return;
534 }
535 }
536
537 throw new Error( `Module ${this.id} does not export ${name} (imported by ${importer.id})` );
538 }
539 }
540
541 parse ( ast ) {
542 // The ast can be supplied programmatically (but usually won't be)
543 if ( !ast ) {
544 // Try to extract a list of top-level statements/declarations. If
545 // the parse fails, attach file info and abort
546 try {
547 ast = parse( this.source, {
548 ecmaVersion: 6,
549 sourceType: 'module',
550 onComment: ( block, text, start, end ) => this.comments.push({ block, text, start, end })
551 });
552 } catch ( err ) {
553 err.code = 'PARSE_ERROR';
554 err.file = this.id; // see above - not necessarily true, but true enough
555 err.message += ` in ${this.id}`;
556 throw err;
557 }
558 }
559
560 walk( ast, {
561 enter: node => {
562 this.magicString.addSourcemapLocation( node.start );
563 this.magicString.addSourcemapLocation( node.end );
564 }
565 });
566
567 let statements = [];
568 let lastChar = 0;
569 let commentIndex = 0;
570
571 ast.body.forEach( node => {
572 // special case - top-level var declarations with multiple declarators
573 // should be split up. Otherwise, we may end up including code we
574 // don't need, just because an unwanted declarator is included
575 if ( node.type === 'VariableDeclaration' && node.declarations.length > 1 ) {
576 // remove the leading var/let/const... UNLESS the previous node
577 // was also a synthetic node, in which case it'll get removed anyway
578 const lastStatement = statements[ statements.length - 1 ];
579 if ( !lastStatement || !lastStatement.node.isSynthetic ) {
580 this.magicString.remove( node.start, node.declarations[0].start );
581 }
582
583 node.declarations.forEach( declarator => {
584 const { start, end } = declarator;
585
586 const syntheticNode = {
587 type: 'VariableDeclaration',
588 kind: node.kind,
589 start,
590 end,
591 declarations: [ declarator ],
592 isSynthetic: true
593 };
594
595 const statement = new Statement( syntheticNode, this, start, end );
596 statements.push( statement );
597 });
598
599 lastChar = node.end; // TODO account for trailing line comment
600 }
601
602 else {
603 let comment;
604 do {
605 comment = this.comments[ commentIndex ];
606 if ( !comment ) break;
607 if ( comment.start > node.start ) break;
608 commentIndex += 1;
609 } while ( comment.end < lastChar );
610
611 const start = comment ? Math.min( comment.start, node.start ) : node.start;
612 const end = node.end; // TODO account for trailing line comment
613
614 const statement = new Statement( node, this, start, end );
615 statements.push( statement );
616
617 lastChar = end;
618 }
619 });
620
621 statements.forEach( ( statement, i ) => {
622 const nextStatement = statements[ i + 1 ];
623 statement.next = nextStatement ? nextStatement.start : statement.end;
624 });
625
626 return statements;
627 }
628
629 rename ( name, replacement ) {
630 this.replacements[ name ] = replacement;
631 }
632
633 render ( allBundleExports, moduleReplacements ) {
634 let magicString = this.magicString.clone();
635
636 this.statements.forEach( statement => {
637 if ( !statement.isIncluded ) {
638 magicString.remove( statement.start, statement.next );
639 return;
640 }
641
642 // skip `export { foo, bar, baz }`
643 if ( statement.node.type === 'ExportNamedDeclaration' ) {
644 // skip `export { foo, bar, baz }`
645 if ( statement.node.specifiers.length ) {
646 magicString.remove( statement.start, statement.next );
647 return;
648 }
649
650 // skip `export var foo;` if foo is exported
651 if ( isEmptyExportedVarDeclaration( statement.node.declaration, allBundleExports, moduleReplacements ) ) {
652 magicString.remove( statement.start, statement.next );
653 return;
654 }
655 }
656
657 // skip empty var declarations for exported bindings
658 // (otherwise we're left with `exports.foo;`, which is useless)
659 if ( isEmptyExportedVarDeclaration( statement.node, allBundleExports, moduleReplacements ) ) {
660 magicString.remove( statement.start, statement.next );
661 return;
662 }
663
664 // split up/remove var declarations as necessary
665 if ( statement.node.isSynthetic ) {
666 // insert `var/let/const` if necessary
667 if ( !allBundleExports[ statement.node.declarations[0].id.name ] ) {
668 magicString.insert( statement.start, `${statement.node.kind} ` );
669 }
670
671 magicString.overwrite( statement.end, statement.next, ';\n' ); // TODO account for trailing newlines
672 }
673
674 let replacements = blank();
675 let bundleExports = blank();
676
677 keys( statement.dependsOn )
678 .concat( keys( statement.defines ) )
679 .forEach( name => {
680 const bundleName = moduleReplacements[ name ] || name;
681
682 if ( allBundleExports[ bundleName ] ) {
683 bundleExports[ name ] = replacements[ name ] = allBundleExports[ bundleName ];
684 } else if ( bundleName !== name ) { // TODO weird structure
685 replacements[ name ] = bundleName;
686 }
687 });
688
689 statement.replaceIdentifiers( magicString, replacements, bundleExports );
690
691 // modify exports as necessary
692 if ( statement.isReexportDeclaration ) {
693 // remove `export { foo } from './other'` and `export * from './other'`
694 magicString.remove( statement.start, statement.next );
695 }
696
697 else if ( statement.isExportDeclaration ) {
698 // remove `export` from `export var foo = 42`
699 if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.declaration.type === 'VariableDeclaration' ) {
700 magicString.remove( statement.node.start, statement.node.declaration.start );
701 }
702
703 // remove `export` from `export class Foo {...}` or `export default Foo`
704 // TODO default exports need different treatment
705 else if ( statement.node.declaration.id ) {
706 magicString.remove( statement.node.start, statement.node.declaration.start );
707 }
708
709 else if ( statement.node.type === 'ExportDefaultDeclaration' ) {
710 const canonicalName = this.defaultName();
711
712 if ( statement.node.declaration.type === 'Identifier' && canonicalName === ( moduleReplacements[ statement.node.declaration.name ] || statement.node.declaration.name ) ) {
713 magicString.remove( statement.start, statement.next );
714 return;
715 }
716
717 // prevent `var undefined = sideEffectyDefault(foo)`
718 if ( canonicalName === undefined ) {
719 magicString.remove( statement.start, statement.node.declaration.start );
720 return;
721 }
722
723 // anonymous functions should be converted into declarations
724 if ( statement.node.declaration.type === 'FunctionExpression' ) {
725 magicString.overwrite( statement.node.start, statement.node.declaration.start + 8, `function ${canonicalName}` );
726 } else {
727 magicString.overwrite( statement.node.start, statement.node.declaration.start, `var ${canonicalName} = ` );
728 }
729 }
730
731 else {
732 throw new Error( 'Unhandled export' );
733 }
734 }
735 });
736
737 return magicString.trim();
738 }
739
740 suggestName ( defaultOrBatch, suggestion ) {
741 // deconflict anonymous default exports with this module's definitions
742 const shouldDeconflict = this.exports.default && this.exports.default.isAnonymous;
743
744 if ( shouldDeconflict ) suggestion = deconflict( suggestion, this.definitions );
745
746 if ( !this.suggestedNames[ defaultOrBatch ] ) {
747 this.suggestedNames[ defaultOrBatch ] = makeLegalIdentifier( suggestion );
748 }
749 }
750}