UNPKG

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