UNPKG

27.8 kBJavaScriptView Raw
1"use strict";
2
3const assert = require("assert");
4const getOption = require("./options.js").get;
5const utils = require("./utils.js");
6
7const MagicString = require("magic-string");
8const Visitor = require("./visitor.js");
9
10const codeOfCR = "\r".charCodeAt(0);
11const codeOfDoubleQuote = '"'.charCodeAt(0);
12const codeOfSingleQuote = "'".charCodeAt(0);
13
14class ImportExportVisitor extends Visitor {
15 finalizeHoisting() {
16 const infoCount = this.bodyInfos.length;
17 const isModule = this.sourceType === "module";
18
19 for (let i = 0; i < infoCount; ++i) {
20 const bodyInfo = this.bodyInfos[i];
21 let codeToInsert = "";
22
23 // We don't need to add a "use strict" directive unless the compiler
24 // made changes or the sourceType is "module".
25 if (bodyInfo.needToAddUseStrictDirective &&
26 (this.madeChanges || isModule)) {
27 this.madeChanges = true;
28 codeToInsert += '"use strict";';
29
30 } else if (this.code &&
31 bodyInfo.insertCharIndex > 0) {
32 const charCode = this.code.charCodeAt(bodyInfo.insertCharIndex - 1)
33 if (charCode === codeOfDoubleQuote ||
34 charCode === codeOfSingleQuote) {
35 // Make sure there's a semicolon after any "use strict"
36 // directives that were already present.
37 codeToInsert += ";";
38 }
39 }
40
41 const addExportsMap = (map, constant) => {
42 const namedExports = toModuleExport(this, map, constant);
43 if (namedExports) {
44 codeToInsert += namedExports;
45 }
46 };
47
48 addExportsMap(bodyInfo.hoistedExportsMap, false);
49 addExportsMap(bodyInfo.hoistedConstExportsMap, true);
50
51 if (bodyInfo.hoistedExportsString) {
52 codeToInsert += bodyInfo.hoistedExportsString;
53 }
54
55 if (bodyInfo.hoistedImportsString) {
56 codeToInsert += bodyInfo.hoistedImportsString;
57 }
58
59 if (codeToInsert) {
60 if (this.magicString !== null) {
61 this.magicString.prependRight(
62 bodyInfo.insertCharIndex,
63 codeToInsert
64 );
65 }
66
67 if (this.modifyAST) {
68 let ast = this.parse(codeToInsert);
69
70 if (ast.type === "File") {
71 ast = ast.program;
72 }
73 assert.strictEqual(ast.type, "Program");
74
75 const spliceArgs = ast.body;
76 spliceArgs.unshift(bodyInfo.insertNodeIndex, 0);
77
78 const body = bodyInfo.body;
79 body.splice.apply(body, spliceArgs);
80
81 const parsedDirectives = ast.directives;
82 const parentDirectives = bodyInfo.parent.directives;
83
84 if (parsedDirectives && parentDirectives) {
85 parentDirectives.push.apply(parentDirectives, parsedDirectives);
86 }
87 }
88 }
89
90 delete bodyInfo.parent._bodyInfoByName;
91 }
92
93 // Just in case we call finalizeHoisting again, don't hoist anything.
94 this.bodyInfos.length = 0;
95
96 if (this.modifyAST) {
97 this.removals.forEach(processRemoval);
98
99 // Just in case we call finalizeHoisting again, don't remove anything.
100 this.removals.length = 0;
101 }
102
103 this.maybeWrapModuleWithFunction();
104 }
105
106 maybeWrapModuleWithFunction() {
107 if (
108 this.finalCompilationPass &&
109 this.moduleAlias !== "module"
110 ) {
111 if (this.magicString !== null) {
112 this.magicString.prepend("!function(" + this.moduleAlias + "){");
113 this.magicString.append("//*/\n}.call(this,module);");
114 }
115
116 if (this.modifyAST) {
117 const id = name => ({ type: "Identifier", name });
118
119 let ast = this.rootPath.getValue();
120 if (ast.type !== "Program") {
121 assert.strictEqual(ast.type, "File");
122 ast = ast.program;
123 }
124
125 ast.body = [{
126 type: "ExpressionStatement",
127 expression: {
128 type: "UnaryExpression",
129 operator: "!",
130 argument: {
131 type: "CallExpression",
132 callee: {
133 type: "MemberExpression",
134 object: {
135 type: "FunctionExpression",
136 id: null,
137 params: [id(this.moduleAlias)],
138 body: {
139 type: "BlockStatement",
140 body: ast.body
141 }
142 },
143 property: id("call")
144 },
145 arguments: [
146 { type: "ThisExpression" },
147 id("module"),
148 ]
149 }
150 }
151 }];
152 }
153 }
154 }
155
156 reset(rootPath, codeOrNull, options) {
157 if (typeof codeOrNull === "string") {
158 this.code = codeOrNull;
159 this.magicString = new MagicString(codeOrNull);
160 } else {
161 this.code = this.magicString = null;
162 }
163
164 this.rootPath = rootPath;
165 this.bodyInfos = [];
166 this.enforceStrictMode = getOption(options, "enforceStrictMode");
167 this.exportedLocalNames = Object.create(null);
168
169 // When compiling for older browsers, it's important to avoid arrow
170 // functions, const/let declarations, and concise method syntax,
171 // especially since the second pass of the Reify Babel plugin
172 // (reify/plugins/babel) runs after most other Babel plugins have run,
173 // so modern syntax may not have a chance to be compiled.
174 this.avoidModernSyntax = !! getOption(options, "avoidModernSyntax");
175 if (this.avoidModernSyntax) {
176 this.generateLetDeclarations = false;
177 } else {
178 // It can be useful to avoid let declarations specifically, even if
179 // this.avoidModernSyntax is false. For example, in the REPL, using
180 // var declarations allows reimporting the same identifiers more
181 // than once, whereas let declarations throw an exception if the
182 // identifer has already been declared, which is utterly unhelpful.
183 this.generateLetDeclarations =
184 !! getOption(options, "generateLetDeclarations");
185 }
186
187 // Controls whether finalizeHoisting performs one-time-only transforms like
188 // wrapping the module body in a function. Defaults to true.
189 this.finalCompilationPass = getOption(options, "finalCompilationPass");
190
191 this.madeChanges = false;
192 this.modifyAST = !! getOption(options, "ast");
193 this.nextKey = 0;
194 this.parse = getOption(options, "parse");
195 this.removals = [];
196 this.sourceType = getOption(options, "sourceType");
197 this.moduleAlias = getOption(options, "moduleAlias");
198 this.possibleIndexes = getOption(options, "possibleIndexes");
199 const di = this.dynamicImport = getOption(options, "dynamicImport");
200 if (di && typeof di !== "string") {
201 // If someone passes { dynamicImport: true }, use module.dynamicImport
202 // by default.
203 this.dynamicImport = "dynamicImport";
204 }
205 }
206
207 visitProgram(path) {
208 this.visitChildren(path);
209 const program = path.getNode();
210 if (program.body.length) {
211 path.call(
212 firstStmtPath => getBlockBodyInfo(this, firstStmtPath),
213 "body", 0
214 );
215 } else {
216 getBlockBodyInfo(this, path);
217 }
218 }
219
220 visitImportDeclaration(path) {
221 const decl = path.getValue();
222 const specifierCount = decl.specifiers.length;
223 let hoistedCode = "";
224
225 if (specifierCount) {
226 const identifiers = [];
227 for (let i = 0; i < specifierCount; ++i) {
228 identifiers.push(decl.specifiers[i].local.name);
229 }
230
231 const identifierCount = identifiers.length;
232 if (identifierCount) {
233 const lastIndex = identifierCount - 1;
234 hoistedCode += this.generateLetDeclarations ? "let " : "var ";
235
236 for (let i = 0; i < identifierCount; ++i) {
237 const isLast = i === lastIndex;
238 hoistedCode +=
239 identifiers[i] +
240 (isLast ? ";" : ",");
241 }
242 }
243 }
244
245 hoistedCode += toModuleImport(
246 this,
247 getSourceString(this, decl),
248 computeSpecifierMap(decl.specifiers)
249 );
250
251 hoistImports(this, path, hoistedCode);
252 }
253
254 visitImport(path) {
255 if (this.dynamicImport) {
256 const importCallee = path.getValue();
257 const replacement = this.moduleAlias + "." + this.dynamicImport;
258 overwrite(this, importCallee.start, importCallee.end, replacement);
259 if (this.modifyAST) {
260 path.replace({
261 type: "MemberExpression",
262 object: {
263 type: "Identifier",
264 name: this.moduleAlias
265 },
266 property: {
267 type: "Identifier",
268 name: this.dynamicImport
269 },
270 computed: false
271 });
272 }
273 this.madeChanges = true;
274 }
275 }
276
277 visitExportAllDeclaration(path) {
278 const decl = path.getValue();
279 const hoistedCode = pad(
280 this,
281 this.moduleAlias + ".link(" + getSourceString(this, decl),
282 decl.start,
283 decl.source.start
284 ) + pad(
285 this,
286 ',{"*":"*"},' + makeUniqueKey(this) + ");",
287 decl.source.end,
288 decl.end
289 );
290
291 // Although this is an export declaration, it also imports the source
292 // module, and uses the module.link API, so it should be hoisted as if
293 // it was an import declaration.
294 hoistImports(this, path, hoistedCode);
295 }
296
297 visitExportDefaultDeclaration(path) {
298 const decl = path.getValue();
299 const dd = decl.declaration;
300
301 if (dd.id && (dd.type === "FunctionDeclaration" ||
302 dd.type === "FunctionExpression" ||
303 dd.type === "ClassDeclaration")) {
304 // If the exported default value is a function or class declaration,
305 // it's important that the declaration be visible to the rest of the
306 // code in the exporting module, so we must avoid compiling it to a
307 // named function or class expression.
308 if (this.modifyAST && dd.type === "FunctionExpression") {
309 dd.type = "FunctionDeclaration";
310 }
311
312 hoistExports(this, path, {
313 "default": [dd.id.name]
314 }, "declaration");
315
316 } else {
317 // Otherwise, since the exported value is an expression, we use the
318 // special module.exportDefault(value) form.
319
320 path.call(this.visitWithoutReset, "declaration");
321 assert.strictEqual(decl.declaration, dd);
322
323 let prefix = this.moduleAlias + ".exportDefault(";
324 let suffix = ");";
325
326 if (dd.type === "SequenceExpression") {
327 // If the exported expression is a comma-separated sequence
328 // expression, this.code.slice(dd.start, dd.end) may not include
329 // the vital parentheses, so we should wrap the expression with
330 // parentheses to make absolutely sure it is treated as a single
331 // argument to the module.exportDefault method, rather than as
332 // multiple arguments.
333 prefix += "(";
334 suffix = ")" + suffix;
335 }
336
337 overwrite(this, decl.start, dd.start, prefix);
338 overwrite(this, dd.end, decl.end, suffix, true);
339
340 if (this.modifyAST) {
341 // A Function or Class declaration has become an expression on the
342 // right side of the exportDefaultPrefix assignment above so
343 // change the AST appropriately
344 if (dd.type === "FunctionDeclaration") {
345 dd.type = "FunctionExpression";
346 } else if (dd.type === "ClassDeclaration") {
347 dd.type = "ClassExpression";
348 }
349
350 // Almost every JS parser parses this expression the same way, but
351 // we should still give custom parsers a chance to parse it.
352 let ast = this.parse(this.moduleAlias + ".exportDefault(ARG);");
353 if (ast.type === "File") ast = ast.program;
354 assert.strictEqual(ast.type, "Program");
355
356 const callExprStmt = ast.body[0];
357 assert.strictEqual(callExprStmt.type, "ExpressionStatement");
358
359 const callExpr = callExprStmt.expression;
360 assert.strictEqual(callExpr.type, "CallExpression");
361
362 // Replace the ARG identifier with the exported expression.
363 callExpr.arguments[0] = dd;
364
365 path.replace(callExprStmt);
366 }
367
368 this.madeChanges = true;
369 }
370 }
371
372 visitExportNamedDeclaration(path) {
373 const decl = path.getValue();
374 const dd = decl.declaration;
375
376 if (dd) {
377 const specifierMap = Object.create(null);
378 const type = dd.type;
379
380 if (dd.id && (type === "ClassDeclaration" ||
381 type === "FunctionExpression" ||
382 type === "FunctionDeclaration")) {
383 addNameToMap(specifierMap, dd.id.name);
384 } else if (type === "VariableDeclaration") {
385 const ddCount = dd.declarations.length;
386
387 for (let i = 0; i < ddCount; ++i) {
388 const names = utils.getNamesFromPattern(dd.declarations[i].id);
389 const nameCount = names.length;
390
391 for (let j = 0; j < nameCount; ++j) {
392 addNameToMap(specifierMap, names[j]);
393 }
394 }
395 }
396
397 hoistExports(this, path, specifierMap, "declaration");
398
399 if (canExportedValuesChange(decl)) {
400 // We can skip adding declared names to this.exportedLocalNames if
401 // the declaration was a const-kinded VariableDeclaration, because
402 // the assignmentVisitor will not need to worry about changes to
403 // these variables.
404 addExportedLocalNames(this, specifierMap);
405 }
406
407 return;
408 }
409
410 if (decl.specifiers) {
411 let specifierMap = computeSpecifierMap(decl.specifiers);
412
413 if (decl.source) {
414 if (specifierMap) {
415 const newMap = Object.create(null);
416 const keys = Object.keys(specifierMap);
417 const keyCount = keys.length;
418
419 for (let i = 0; i < keyCount; ++i) {
420 const exported = keys[i];
421 const locals = specifierMap[exported];
422 const localCount = locals.length;
423
424 for (let j = 0; j < localCount; ++j) {
425 addToSpecifierMap(newMap, locals[j], "exports." + exported);
426 }
427 }
428
429 specifierMap = newMap;
430 }
431
432 // Although this is an export declaration, it also imports the
433 // source module, and uses the module.link API, so it should be
434 // hoisted as if it was an import declaration.
435 hoistImports(this, path, toModuleImport(
436 this,
437 getSourceString(this, decl),
438 specifierMap
439 ));
440
441 } else {
442 hoistExports(this, path, specifierMap);
443 addExportedLocalNames(this, specifierMap);
444 }
445 }
446 }
447};
448
449function addExportedLocalNames(visitor, specifierMap) {
450 const exportedLocalNames = visitor.exportedLocalNames;
451 const keys = Object.keys(specifierMap);
452 const keyCount = keys.length;
453
454 for (let i = 0; i < keyCount; ++i) {
455 const exported = keys[i];
456 const locals = specifierMap[exported];
457 const localCount = locals.length;
458
459 for (let j = 0; j < localCount; ++j) {
460 // It's tempting to record the exported name as the value here,
461 // instead of true, but there can be more than one exported name
462 // per local variable, and we don't actually use the exported
463 // name(s) in the assignmentVisitor, so it's not worth the added
464 // complexity of tracking unused information.
465 exportedLocalNames[locals[j]] = true;
466 }
467 }
468}
469
470function addNameToMap(map, name) {
471 addToSpecifierMap(map, name, name);
472}
473
474function addToSpecifierMap(map, __ported, local) {
475 assert.strictEqual(typeof __ported, "string");
476 assert.strictEqual(typeof local, "string");
477
478 const locals = __ported in map ? map[__ported] : [];
479
480 if (locals.indexOf(local) < 0) {
481 locals.push(local);
482 }
483
484 map[__ported] = locals;
485
486 return map;
487}
488
489// Returns a map from {im,ex}ported identifiers to lists of local variable
490// names bound to those identifiers.
491function computeSpecifierMap(specifiers) {
492 const specifierCount = specifiers.length;
493 const specifierMap = Object.create(null);
494
495 for (let i = 0; i < specifierCount; ++i) {
496 const s = specifiers[i];
497
498 const local =
499 s.type === "ExportDefaultSpecifier" ? "default" :
500 s.type === "ExportNamespaceSpecifier" ? "*" :
501 s.local.name;
502
503 const __ported = // The IMported or EXported name.
504 s.type === "ImportSpecifier" ? s.imported.name :
505 s.type === "ImportDefaultSpecifier" ? "default" :
506 s.type === "ImportNamespaceSpecifier" ? "*" :
507 (s.type === "ExportSpecifier" ||
508 s.type === "ExportDefaultSpecifier" ||
509 s.type === "ExportNamespaceSpecifier") ? s.exported.name :
510 null;
511
512 if (typeof local === "string" && typeof __ported === "string") {
513 addToSpecifierMap(specifierMap, __ported, local);
514 }
515 }
516
517 return specifierMap;
518}
519
520function getBlockBodyInfo(visitor, path) {
521 const node = path.getNode();
522 let parent = path.getParentNode();
523
524 if (parent === null) {
525 parent = node;
526 }
527
528 let body = parent.body;
529 let bodyName = "body";
530 let insertCharIndex = node.start;
531 let needToAddUseStrictDirective = false;
532
533 switch (parent.type) {
534 case "Program":
535 insertCharIndex = parent.start;
536
537 // If parent is a Program, we may need to add a "use strict"
538 // directive to enable const/let in Node 4.
539 needToAddUseStrictDirective = visitor.enforceStrictMode;
540 break;
541
542 case "BlockStatement":
543 insertCharIndex = parent.start + 1;
544 break;
545
546 case "SwitchCase":
547 body = parent.consequent;
548 bodyName = "consequent";
549 insertCharIndex = body[0].start;
550 break;
551
552 default:
553 const block = {
554 type: "BlockStatement",
555 body: [],
556 start: node.start,
557 end: node.end + 2
558 };
559
560 body = block.body;
561 bodyName = path.getName();
562 insertCharIndex = node.start;
563
564 if (visitor.magicString !== null) {
565 visitor.magicString
566 .appendLeft(insertCharIndex, "{")
567 .prependRight(node.end, "}");
568 }
569
570 if (visitor.modifyAST) {
571 path.replace(block);
572 }
573 }
574
575 assert.ok(Array.isArray(body), body);
576
577 // Avoid hoisting above string literal expression statements such as
578 // "use strict", which may depend on occurring at the beginning of
579 // their enclosing scopes.
580 let insertNodeIndex = 0;
581 const stmtCount = body.length;
582
583 for (let i = 0; i < stmtCount; ++i) {
584 const stmt = body[i];
585 if (stmt.type === "ExpressionStatement") {
586 const expr = stmt.expression;
587 if (expr.type === "StringLiteral" ||
588 (expr.type === "Literal" &&
589 typeof expr.value === "string")) {
590 insertCharIndex = stmt.end;
591 insertNodeIndex = i + 1;
592 if (expr.value === "use strict") {
593 // If there's already a "use strict" directive, then we don't
594 // need to add another one.
595 needToAddUseStrictDirective = false;
596 }
597 continue;
598 }
599 }
600 break;
601 }
602
603 // Babylon represents directives like "use strict" with a .directives
604 // array property on the parent node.
605 const directives = parent.directives;
606 const directiveCount = directives ? directives.length : 0;
607
608 for (let i = 0; i < directiveCount; ++i) {
609 const d = directives[i];
610 insertCharIndex = Math.max(d.end, insertCharIndex);
611 if (d.value.value === "use strict") {
612 // If there's already a "use strict" directive, then we don't
613 // need to add another one.
614 needToAddUseStrictDirective = false;
615 }
616 }
617
618 let bibn = parent._bodyInfoByName;
619 if (bibn === void 0) {
620 bibn = parent._bodyInfoByName = Object.create(null);
621 }
622
623 let bodyInfo = bibn[bodyName];
624 if (bodyInfo === void 0) {
625 bodyInfo = bibn[bodyName] = Object.create(null);
626 bodyInfo.body = body;
627 bodyInfo.parent = parent;
628 bodyInfo.insertCharIndex = insertCharIndex;
629 bodyInfo.insertNodeIndex = insertNodeIndex;
630 bodyInfo.hoistedExportsMap = Object.create(null);
631 bodyInfo.hoistedConstExportsMap = Object.create(null);
632 bodyInfo.hoistedExportsString = "";
633 bodyInfo.hoistedImportsString = "";
634 } else {
635 assert.strictEqual(bodyInfo.body, body);
636 }
637
638 if (visitor.bodyInfos.indexOf(bodyInfo) < 0) {
639 visitor.bodyInfos.push(bodyInfo);
640 }
641
642 if (needToAddUseStrictDirective) {
643 bodyInfo.needToAddUseStrictDirective = needToAddUseStrictDirective;
644 }
645
646 return bodyInfo;
647}
648
649// Gets a string representation (including quotes) from an import or
650// export declaration node.
651function getSourceString(visitor, decl) {
652 const code = visitor.code;
653 if (code) {
654 return code.slice(
655 decl.source.start,
656 decl.source.end
657 );
658 }
659
660 assert.strictEqual(typeof decl.source.value, "string");
661
662 return JSON.stringify(decl.source.value);
663}
664
665function hoistImports(visitor, importDeclPath, hoistedCode) {
666 preserveLine(visitor, importDeclPath);
667 const bodyInfo = getBlockBodyInfo(visitor, importDeclPath);
668 bodyInfo.hoistedImportsString += hoistedCode;
669 visitor.madeChanges = true;
670}
671
672function hoistExports(visitor, exportDeclPath, mapOrString, childName) {
673 if (childName) {
674 preserveChild(visitor, exportDeclPath, childName);
675 } else {
676 preserveLine(visitor, exportDeclPath);
677 }
678
679 const bodyInfo = getBlockBodyInfo(visitor, exportDeclPath);
680 const constant = ! canExportedValuesChange(exportDeclPath.getValue());
681
682 if (typeof mapOrString !== "string") {
683 const keys = Object.keys(mapOrString);
684 const keyCount = keys.length;
685
686 for (let i = 0; i < keyCount; ++i) {
687 const exported = keys[i];
688 const locals = mapOrString[exported];
689 const localCount = locals.length;
690
691 for (let j = 0; j < localCount; ++j) {
692 addToSpecifierMap(
693 constant
694 ? bodyInfo.hoistedConstExportsMap
695 : bodyInfo.hoistedExportsMap,
696 exported,
697 locals[j]
698 );
699 }
700 }
701
702 } else {
703 bodyInfo.hoistedExportsString += mapOrString;
704 }
705
706 visitor.madeChanges = true;
707}
708
709function canExportedValuesChange(exportDecl) {
710 if (exportDecl) {
711 if (exportDecl.type === "ExportDefaultDeclaration") {
712 const dd = exportDecl.declaration;
713 return (dd.type === "FunctionDeclaration" ||
714 dd.type === "FunctionExpression" ||
715 dd.type === "ClassDeclaration");
716 }
717
718 if (exportDecl.type === "ExportNamedDeclaration") {
719 const dd = exportDecl.declaration;
720 if (dd &&
721 dd.type === "VariableDeclaration" &&
722 dd.kind === "const") {
723 return false;
724 }
725 }
726 }
727
728 return true;
729}
730
731function makeUniqueKey(visitor) {
732 return visitor.nextKey++;
733}
734
735function overwrite(visitor, oldStart, oldEnd, newCode, trailing) {
736 if (! visitor.code) {
737 return;
738 }
739
740 assert.strictEqual(typeof oldStart, "number");
741 assert.strictEqual(typeof oldEnd, "number");
742 assert.strictEqual(typeof newCode, "string");
743
744 const padded = pad(visitor, newCode, oldStart, oldEnd);
745
746 if (oldStart === oldEnd) {
747 if (padded === "") {
748 return;
749 }
750
751 if (trailing) {
752 visitor.magicString.appendLeft(oldStart, padded);
753 } else {
754 visitor.magicString.prependRight(oldStart, padded);
755 }
756
757 } else {
758 visitor.magicString.overwrite(oldStart, oldEnd, padded);
759 }
760}
761
762function pad(visitor, newCode, oldStart, oldEnd) {
763 const code = visitor.code;
764 if (code) {
765 const oldLines = code.slice(oldStart, oldEnd).split("\n");
766 const oldLineCount = oldLines.length;
767 const newLines = newCode.split("\n");
768 const lastIndex = newLines.length - 1;
769
770 for (let i = lastIndex; i < oldLineCount; ++i) {
771 const oldLine = oldLines[i];
772 const lastCharCode = oldLine.charCodeAt(oldLine.length - 1);
773 if (i > lastIndex) {
774 newLines[i] = "";
775 }
776 if (lastCharCode === codeOfCR) {
777 newLines[i] += "\r";
778 }
779 }
780
781 newCode = newLines.join("\n");
782 }
783
784 return newCode;
785}
786
787function preserveChild(visitor, path, childName) {
788 const value = path.getValue();
789 const child = value ? value[childName] : null;
790
791 if (child && visitor.code) {
792 overwrite(
793 visitor,
794 value.start,
795 child.start,
796 ""
797 );
798 overwrite(
799 visitor,
800 child.end,
801 value.end,
802 ""
803 );
804 }
805
806 path.call(visitor.visitWithoutReset, childName);
807
808 if (visitor.modifyAST) {
809 // Replace the given path with the child we want to preserve.
810 path.replace(child);
811 }
812}
813
814function preserveLine(visitor, path) {
815 const value = path.getValue();
816
817 if (visitor.code) {
818 overwrite(visitor, value.start, value.end, "");
819 }
820
821 if (visitor.modifyAST) {
822 visitor.removals.push({
823 container: path.getContainer(),
824 name: path.getName(),
825 value
826 });
827 }
828}
829
830function processRemoval(removal) {
831 if (Array.isArray(removal.container)) {
832 const index = removal.container.indexOf(removal.value);
833 if (index >= 0) {
834 removal.container.splice(index, 1);
835 }
836
837 } else if (removal.value ===
838 removal.container[removal.name]) {
839 // This case is almost certainly never reached.
840 removal.container[removal.name] = null;
841
842 } else {
843 const newValue = removal.container[removal.name];
844
845 if (newValue.type === "BlockStatement") {
846 // This newValue is a BlockStatement that we created in the default
847 // case of the switch statement in getBlockBodyInfo, so we make sure
848 // the original import/export declaration is no longer in its .body.
849 processRemoval({
850 container: newValue.body,
851 value: removal.value
852 });
853 }
854 }
855}
856
857function safeKey(key) {
858 if (/^[_$a-zA-Z]\w*$/.test(key)) {
859 return key;
860 }
861 return JSON.stringify(key);
862}
863
864function safeParam(param, locals) {
865 if (locals.indexOf(param) < 0) {
866 return param;
867 }
868 return safeParam("_" + param, locals);
869}
870
871function toModuleImport(visitor, source, specifierMap) {
872 let code = visitor.moduleAlias + ".link(" + source;
873 const importedNames = Object.keys(specifierMap);
874 const nameCount = importedNames.length;
875
876 if (! nameCount) {
877 code += ");";
878 return code;
879 }
880
881 const lastIndex = nameCount - 1;
882 code += ",{";
883
884 const properties = [];
885
886 for (let i = 0; i < nameCount; ++i) {
887 const imported = importedNames[i];
888 const reexports = [];
889 const locals = [];
890
891 specifierMap[imported].forEach(local => {
892 if (local.startsWith("exports.")) {
893 var parts = local.split(".");
894 parts.shift();
895 reexports.push(parts.join("."));
896 } else {
897 locals.push(local);
898 }
899 });
900
901 if (locals.length > 0) {
902 let property = safeKey(imported);
903
904 if (visitor.avoidModernSyntax) {
905 property += ":function";
906 }
907
908 const valueParam = safeParam("v", locals);
909 property += "(" + valueParam + "){";
910 // Multiple local variables become a compound assignment.
911 property += locals.join("=") + "=" + valueParam,
912 property += "}"
913
914 properties.push(property);
915 }
916
917 if (reexports.length > 0) {
918 properties.push(
919 safeKey(imported) + ":" + JSON.stringify(
920 // Avoid wrapping single strings as arrays, for brevity:
921 reexports.length === 1 ? reexports[0] : reexports
922 )
923 );
924 }
925 }
926
927 code += properties.join(",") + "}," + makeUniqueKey(visitor) + ");";
928
929 return code;
930}
931
932function toModuleExport(visitor, specifierMap, constant) {
933 let code = "";
934 const exportedKeys = Object.keys(specifierMap);
935 const keyCount = exportedKeys.length;
936
937 if (! keyCount) {
938 return code;
939 }
940
941 const lastIndex = keyCount - 1;
942 code += visitor.moduleAlias + ".export({";
943
944 for (let i = 0; i < keyCount; ++i) {
945 const exported = exportedKeys[i];
946 const isLast = i === lastIndex;
947 const locals = specifierMap[exported];
948
949 assert.strictEqual(locals.length, 1);
950
951 if (visitor.avoidModernSyntax) {
952 code += exported + ":function(){return " + locals[0] + "}";
953 } else {
954 code += exported + ":()=>" + locals[0];
955 }
956
957 if (! isLast) {
958 code += ",";
959 }
960 }
961
962 // The second argument to module.export indicates whether the getter
963 // functions provided in the first argument are constant or not.
964 code += constant ? "},true);" : "});";
965
966 return code;
967}
968
969module.exports = ImportExportVisitor;