1 | import { parse } from 'acorn/src/index.js';
|
2 | import MagicString from 'magic-string';
|
3 | import { walk } from 'estree-walker';
|
4 | import Statement from './Statement.js';
|
5 | import { blank, keys } from './utils/object.js';
|
6 | import { basename, extname } from './utils/path.js';
|
7 | import getLocation from './utils/getLocation.js';
|
8 | import makeLegalIdentifier from './utils/makeLegalIdentifier.js';
|
9 | import SOURCEMAPPING_URL from './utils/sourceMappingURL.js';
|
10 | import {
|
11 | SyntheticDefaultDeclaration,
|
12 | SyntheticGlobalDeclaration,
|
13 | SyntheticNamespaceDeclaration
|
14 | } from './Declaration.js';
|
15 | import { isFalsy, isTruthy } from './ast/conditions.js';
|
16 | import { emptyBlockStatement } from './ast/create.js';
|
17 | import extractNames from './ast/extractNames.js';
|
18 |
|
19 | export 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 |
|
29 | this.sources = [];
|
30 | this.dependencies = [];
|
31 | this.resolvedIds = blank();
|
32 |
|
33 |
|
34 | this.imports = blank();
|
35 | this.exports = blank();
|
36 | this.reexports = blank();
|
37 |
|
38 | this.exportAllSources = [];
|
39 | this.exportAllModules = null;
|
40 |
|
41 |
|
42 |
|
43 | this.magicString = new MagicString( code, {
|
44 | filename: id,
|
45 | indentExclusionRanges: []
|
46 | });
|
47 |
|
48 |
|
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 |
|
69 | if ( source ) {
|
70 | if ( !~this.sources.indexOf( source ) ) this.sources.push( source );
|
71 |
|
72 | if ( node.type === 'ExportAllDeclaration' ) {
|
73 |
|
74 |
|
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
|
85 | };
|
86 | });
|
87 | }
|
88 | }
|
89 |
|
90 |
|
91 |
|
92 |
|
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 |
|
102 | this.declarations.default = new SyntheticDefaultDeclaration( node, statement, identifier || this.basename() );
|
103 | }
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 | else if ( node.type === 'ExportNamedDeclaration' ) {
|
111 | if ( node.specifiers.length ) {
|
112 |
|
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 |
|
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 |
|
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 |
|
239 | if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) {
|
240 |
|
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 |
|
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 |
|
288 | if ( !ast ) {
|
289 |
|
290 |
|
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;
|
301 | err.message += ` in ${this.id}`;
|
302 | throw err;
|
303 | }
|
304 | }
|
305 |
|
306 | walk( ast, {
|
307 | enter: (node, parent, prop) => {
|
308 |
|
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 |
|
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 |
|
368 |
|
369 |
|
370 | if ( node.type === 'VariableDeclaration' && node.declarations.length > 1 ) {
|
371 |
|
372 |
|
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;
|
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;
|
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 |
|
438 | if ( statement.node.type === 'ExportNamedDeclaration' ) {
|
439 | if ( statement.node.isSynthetic ) return;
|
440 |
|
441 |
|
442 | if ( statement.node.specifiers.length ) {
|
443 | magicString.remove( statement.start, statement.next );
|
444 | return;
|
445 | }
|
446 | }
|
447 |
|
448 |
|
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 ) {
|
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 |
|
468 |
|
469 |
|
470 |
|
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 |
|
482 | magicString.insert( statement.start, `${statement.node.kind} ` );
|
483 | magicString.overwrite( statement.end, statement.next, ';\n' );
|
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 |
|
502 |
|
503 | if ( reference.name === name && name.length === end - start ) return;
|
504 |
|
505 | reference.rewritten = true;
|
506 |
|
507 |
|
508 | const identifier = name.match( /[^\.]+/ )[0];
|
509 | if ( reference.scope.contains( identifier ) ) {
|
510 | toDeshadow[ identifier ] = `${identifier}$$`;
|
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 |
|
530 | if ( statement.isExportDeclaration ) {
|
531 |
|
532 |
|
533 |
|
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 |
|
549 | magicString.remove( statement.start, statement.next );
|
550 | }
|
551 |
|
552 |
|
553 |
|
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 |
|
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 |
|
570 | if ( !defaultDeclaration.exportName && !defaultDeclaration.isUsed ) {
|
571 | magicString.remove( statement.start, statement.node.declaration.start );
|
572 | return;
|
573 | }
|
574 |
|
575 |
|
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 |
|
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 |
|
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 | }
|