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 { SyntheticDefaultDeclaration, SyntheticNamespaceDeclaration } from './Declaration.js';
|
11 | import { isFalsy, isTruthy } from './ast/conditions.js';
|
12 | import { emptyBlockStatement } from './ast/create.js';
|
13 |
|
14 | export 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 |
|
24 | this.dependencies = [];
|
25 | this.resolvedIds = blank();
|
26 |
|
27 |
|
28 | this.imports = blank();
|
29 | this.exports = blank();
|
30 | this.reexports = blank();
|
31 |
|
32 | this.exportAllSources = [];
|
33 | this.exportAllModules = null;
|
34 |
|
35 |
|
36 |
|
37 | this.magicString = new MagicString( code, {
|
38 | filename: id,
|
39 | indentExclusionRanges: []
|
40 | });
|
41 |
|
42 |
|
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 |
|
63 | if ( source ) {
|
64 | if ( !~this.dependencies.indexOf( source ) ) this.dependencies.push( source );
|
65 |
|
66 | if ( node.type === 'ExportAllDeclaration' ) {
|
67 |
|
68 |
|
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
|
79 | };
|
80 | });
|
81 | }
|
82 | }
|
83 |
|
84 |
|
85 |
|
86 |
|
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 |
|
96 | this.declarations.default = new SyntheticDefaultDeclaration( node, statement, identifier || this.basename() );
|
97 | }
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 | else if ( node.type === 'ExportNamedDeclaration' ) {
|
104 | if ( node.specifiers.length ) {
|
105 |
|
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 |
|
121 | name = declaration.declarations[0].id.name;
|
122 | } else {
|
123 |
|
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 |
|
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 |
|
224 | if ( statement.node.type === 'ExportNamedDeclaration' && statement.node.specifiers.length ) {
|
225 |
|
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 |
|
237 | this.bundle.assumedGlobals[ reference.name ] = true;
|
238 | }
|
239 | });
|
240 | });
|
241 | }
|
242 |
|
243 | consolidateDependencies () {
|
244 | let strongDependencies = [];
|
245 | let weakDependencies = [];
|
246 |
|
247 |
|
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 |
|
293 | if ( !ast ) {
|
294 |
|
295 |
|
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;
|
306 | err.message += ` in ${this.id}`;
|
307 | throw err;
|
308 | }
|
309 | }
|
310 |
|
311 | walk( ast, {
|
312 | enter: node => {
|
313 |
|
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 |
|
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 |
|
364 |
|
365 |
|
366 | if ( node.type === 'VariableDeclaration' && node.declarations.length > 1 ) {
|
367 |
|
368 |
|
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;
|
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;
|
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 |
|
434 | if ( statement.node.type === 'ExportNamedDeclaration' ) {
|
435 | if ( statement.node.isSynthetic ) return;
|
436 |
|
437 |
|
438 | if ( statement.node.specifiers.length ) {
|
439 | magicString.remove( statement.start, statement.next );
|
440 | return;
|
441 | }
|
442 | }
|
443 |
|
444 |
|
445 | if ( statement.node.isSynthetic ) {
|
446 |
|
447 | const declaration = this.declarations[ statement.node.declarations[0].id.name ];
|
448 | if ( !( declaration.isExported && declaration.isReassigned ) ) {
|
449 | magicString.insert( statement.start, `${statement.node.kind} ` );
|
450 | }
|
451 |
|
452 | magicString.overwrite( statement.end, statement.next, ';\n' );
|
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 |
|
470 |
|
471 | if ( reference.name === name && name.length === end - start ) return;
|
472 |
|
473 | reference.rewritten = true;
|
474 |
|
475 |
|
476 | const identifier = name.match( /[^\.]+/ )[0];
|
477 | if ( reference.scope.contains( identifier ) ) {
|
478 | toDeshadow[ identifier ] = `${identifier}$$`;
|
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 |
|
498 | if ( statement.isExportDeclaration ) {
|
499 |
|
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 |
|
513 | magicString.remove( statement.start, statement.next );
|
514 | }
|
515 |
|
516 |
|
517 |
|
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 |
|
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 |
|
534 | if ( !defaultDeclaration.isExported && !defaultDeclaration.isUsed ) {
|
535 | magicString.remove( statement.start, statement.node.declaration.start );
|
536 | return;
|
537 | }
|
538 |
|
539 |
|
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 |
|
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 |
|
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 | }
|