UNPKG

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