UNPKG

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