UNPKG

@meteorjs/reify

Version:

Enable ECMAScript 2015 modules in Node today. No caveats. Full stop.

1,138 lines (959 loc) 34 kB
"use strict"; const assert = require("assert"); const getOption = require("./options.js").get; const utils = require("./utils.js"); const MagicString = require("magic-string"); const Visitor = require("./visitor.js"); const { isTopLevelAwait } = require('./is-top-level-await'); const codeOfCR = "\r".charCodeAt(0); const codeOfDoubleQuote = '"'.charCodeAt(0); const codeOfSingleQuote = "'".charCodeAt(0); class ImportExportVisitor extends Visitor { finalizeHoisting() { const infoCount = this.bodyInfos.length; const isModule = this.sourceType === "module"; for (let i = 0; i < infoCount; ++i) { const bodyInfo = this.bodyInfos[i]; let codeToInsert = ""; let hasImports = false; // We don't need to add a "use strict" directive unless the compiler // made changes or the sourceType is "module". let addUseStrict = bodyInfo.needToAddUseStrictDirective && (this.madeChanges || isModule) if (!addUseStrict && this.code && bodyInfo.insertCharIndex > 0) { const charCode = this.code.charCodeAt(bodyInfo.insertCharIndex - 1) if (charCode === codeOfDoubleQuote || charCode === codeOfSingleQuote) { // Make sure there's a semicolon after any "use strict" // directives that were already present. codeToInsert += ";"; } } const addExportsMap = (map, constant) => { const namedExports = toModuleExport(this, map, constant); if (namedExports) { codeToInsert += namedExports; } }; addExportsMap(bodyInfo.hoistedExportsMap, false); addExportsMap(bodyInfo.hoistedConstExportsMap, true); if (bodyInfo.hoistedExportsString) { codeToInsert += bodyInfo.hoistedExportsString; } if (bodyInfo.hoistedImportsString) { hasImports = true; codeToInsert += bodyInfo.hoistedImportsString; } if ( this.prepareForTopLevelAwait && bodyInfo.parent.type === 'Program' && (hasImports || this.hasTopLevelAwait) && !this.wrapForTopLevelAwait ) { this.wrapForTopLevelAwait = true; if (hasImports) { codeToInsert += `if (__reifyWaitForDeps__()) (await __reifyWaitForDeps__())();` } else { // Even though we don't need to wait for deps since there are none, // we still need to call the function to indicate that linking is done codeToInsert += `__reifyWaitForDeps__();`; } } else if (addUseStrict) { // All modules that are wrapped for top level await (use imports or // have top level await) have "use strict" added when wrapped // so we skip them here. codeToInsert = '"use strict";' + codeToInsert; } if (codeToInsert) { if (this.magicString !== null) { this.magicString.prependRight( bodyInfo.insertCharIndex, codeToInsert ); } if (this.modifyAST) { let ast = this.parse(codeToInsert); if (ast.type === "File") { ast = ast.program; } assert.strictEqual(ast.type, "Program"); const spliceArgs = ast.body; spliceArgs.unshift(bodyInfo.insertNodeIndex, 0); const body = bodyInfo.body; body.splice.apply(body, spliceArgs); const parsedDirectives = ast.directives; const parentDirectives = bodyInfo.parent.directives; if (parsedDirectives && parentDirectives) { parentDirectives.push.apply(parentDirectives, parsedDirectives); } } } delete bodyInfo.parent._bodyInfoByName; } // Just in case we call finalizeHoisting again, don't hoist anything. this.bodyInfos.length = 0; if (this.modifyAST) { this.removals.forEach(processRemoval); // Just in case we call finalizeHoisting again, don't remove anything. this.removals.length = 0; } this.maybeWrapModuleWithFunction(); } maybeWrapModuleWithFunction() { if ( this.finalCompilationPass && ( this.moduleAlias !== "module" || this.wrapForTopLevelAwait )) { if (this.magicString !== null) { if (this.wrapForTopLevelAwait) { this.magicString.prepend('!module.wrapAsync(async function (' + this.moduleAlias + ', __reifyWaitForDeps__, __reifyAsyncResult__) {"use strict"; try {'); this.magicString.append(`//*/\n__reifyAsyncResult__();} catch (_reifyError) { __reifyAsyncResult__(_reifyError); }}, { self: this, async: ${this.hasTopLevelAwait} });`); } else { this.magicString.prepend("!function(" + this.moduleAlias + "){"); this.magicString.append("//*/\n}.call(this,module);"); } } if (this.modifyAST) { const id = name => ({ type: "Identifier", name }); let ast = this.rootPath.getValue(); if (ast.type !== "Program") { assert.strictEqual(ast.type, "File"); ast = ast.program; } if (this.wrapForTopLevelAwait) { let originalBody = ast.body; originalBody.push({ type: "ExpressionStatement", expression: { type: "CallExpression", callee: { type: "Identifier", name: "__reify_async_result__" }, arguments: [], } }); ast.body = [{ type: "ExpressionStatement", expression: { type: "UnaryExpression", operator: "!", argument: { type: "CallExpression", callee: { type: "MemberExpression", object: id('module'), property: id("wrapAsync") }, arguments: [ { type: "FunctionExpression", id: null, async: true, params: [ id(this.moduleAlias), id('__reifyWaitForDeps__'), id('__reify_async_result__') ], body: { type: "BlockStatement", body: [ { type: "ExpressionStatement", expression: { type: "StringLiteral", value: "use strict" } }, { type: "TryStatement", block: { type: 'BlockStatement', body: originalBody, }, handler: { type: 'CatchClause', param: id('_reifyError'), body: { type: 'BlockStatement', body: [ { type: 'ReturnStatement', argument: { type: 'CallExpression', callee: id('__reify_async_result__'), arguments: [ id('_reifyError') ] } } ] } } }, { type: 'CallExpression', callee: id('__reify_async_result__'), arguments: [] } ] } }, { type: "ObjectExpression", properties: [ { type: "ObjectProperty", key: { type: "Identifier", name: "self" }, value: { type: "ThisExpression" } }, { type: "ObjectProperty", key: { type: "Identifier", name: "async" }, value: { type: "BooleanLiteral", value: this.hasTopLevelAwait } } ] } ] } } }]; } else { ast.body = [{ type: "ExpressionStatement", expression: { type: "UnaryExpression", operator: "!", argument: { type: "CallExpression", callee: { type: "MemberExpression", object: { type: "FunctionExpression", id: null, params: [id(this.moduleAlias)], body: { type: "BlockStatement", body: ast.body } }, property: id("call") }, arguments: [ { type: "ThisExpression" }, id("module"), ] } } }]; } } } } reset(rootPath, codeOrNull, options) { if (typeof codeOrNull === "string") { this.code = codeOrNull; this.magicString = new MagicString(codeOrNull); } else { this.code = this.magicString = null; } this.rootPath = rootPath; this.bodyInfos = []; this.enforceStrictMode = getOption(options, "enforceStrictMode"); this.exportedLocalNames = Object.create(null); this.wrapForTopLevelAwait = getOption(options, "forceWrapForTopLevelAwait"); // When compiling for older browsers, it's important to avoid arrow // functions, const/let declarations, and concise method syntax, // especially since the second pass of the Reify Babel plugin // (reify/plugins/babel) runs after most other Babel plugins have run, // so modern syntax may not have a chance to be compiled. this.avoidModernSyntax = !! getOption(options, "avoidModernSyntax"); if (this.avoidModernSyntax) { this.generateLetDeclarations = false; } else { // It can be useful to avoid let declarations specifically, even if // this.avoidModernSyntax is false. For example, in the REPL, using // var declarations allows reimporting the same identifiers more // than once, whereas let declarations throw an exception if the // identifer has already been declared, which is utterly unhelpful. this.generateLetDeclarations = !! getOption(options, "generateLetDeclarations"); } // Controls whether finalizeHoisting performs one-time-only transforms like // wrapping the module body in a function. Defaults to true. this.finalCompilationPass = getOption(options, "finalCompilationPass"); this.madeChanges = false; this.prepareForTopLevelAwait = getOption(options, "topLevelAwait"); this.hasTopLevelAwait = getOption(options, "forceHasTopLevelAwait"); this.modifyAST = !! getOption(options, "ast"); this.nextKey = 0; this.parse = getOption(options, "parse"); this.removals = []; this.sourceType = getOption(options, "sourceType"); this.moduleAlias = getOption(options, "moduleAlias"); this.possibleIndexes = getOption(options, "possibleIndexes"); const di = this.dynamicImport = getOption(options, "dynamicImport"); if (di && typeof di !== "string") { // If someone passes { dynamicImport: true }, use module.dynamicImport // by default. this.dynamicImport = "dynamicImport"; } } visitProgram(path) { this.visitChildren(path); const program = path.getNode(); if (program.body.length) { path.call( firstStmtPath => getBlockBodyInfo(this, firstStmtPath), "body", 0 ); } else { getBlockBodyInfo(this, path); } } visitImportDeclaration(path) { const decl = path.getValue(); const specifierCount = decl.specifiers.length; let hoistedCode = ""; if (specifierCount) { const identifiers = []; for (let i = 0; i < specifierCount; ++i) { identifiers.push(decl.specifiers[i].local.name); } const identifierCount = identifiers.length; if (identifierCount) { const lastIndex = identifierCount - 1; hoistedCode += this.generateLetDeclarations ? "let " : "var "; for (let i = 0; i < identifierCount; ++i) { const isLast = i === lastIndex; hoistedCode += identifiers[i] + (isLast ? ";" : ","); } } } let source = getSourceString(this, decl); hoistedCode += toModuleImport( this, source, computeSpecifierMap(decl.specifiers) ); hoistImports(this, path, hoistedCode); } visitAwaitExpression(path) { if (!this.hasTopLevelAwait) { const parent = path.getParentNode(1); const node = path.getNode(); if ( parent.type === 'ExpressionStatement' && path.getParentNode(2).type === 'Program' ) { this.hasTopLevelAwait = true; } else if (node.argument.type === "CallExpression" && node.argument.callee.type === "Identifier" && node.argument.callee.name === "__reifyWaitForDeps__" ) { // Do not treat the emitted "await __reifyWaitForDeps__()" as a TLA trigger // This happens e.g. for the package refapp:meteor-typescript which subclasses // BabelCompiler } else { this.hasTopLevelAwait = isTopLevelAwait(path.stack); } } // There could be children with import expressions this.visitChildren(path) } // This is also aliased as visitImport for Babel's AST visitImportExpression(path) { if (this.dynamicImport) { const importCallee = path.getValue(); const replacement = this.moduleAlias + "." + this.dynamicImport; // "import" is 6 characters let end = importCallee.start + 6; overwrite(this, importCallee.start, end, replacement); if (this.modifyAST) { path.replace({ type: "MemberExpression", object: { type: "Identifier", name: this.moduleAlias }, property: { type: "Identifier", name: this.dynamicImport }, computed: false }); } this.madeChanges = true; } } visitExportAllDeclaration(path) { const decl = path.getValue(); const hoistedCode = pad( this, this.moduleAlias + ".link(" + getSourceString(this, decl), decl.start, decl.source.start ) + pad( this, ',{"*":"*"},' + makeUniqueKey(this) + ");", decl.source.end, decl.end ); // Although this is an export declaration, it also imports the source // module, and uses the module.link API, so it should be hoisted as if // it was an import declaration. hoistImports(this, path, hoistedCode); } visitExportDefaultDeclaration(path) { const decl = path.getValue(); const dd = decl.declaration; if (dd.id && (dd.type === "FunctionDeclaration" || dd.type === "FunctionExpression" || dd.type === "ClassDeclaration")) { // If the exported default value is a function or class declaration, // it's important that the declaration be visible to the rest of the // code in the exporting module, so we must avoid compiling it to a // named function or class expression. if (this.modifyAST && dd.type === "FunctionExpression") { dd.type = "FunctionDeclaration"; } hoistExports(this, path, { "default": [dd.id.name] }, "declaration"); } else { // Otherwise, since the exported value is an expression, we use the // special module.exportDefault(value) form. path.call(this.visitWithoutReset, "declaration"); assert.strictEqual(decl.declaration, dd); let prefix = this.moduleAlias + ".exportDefault("; let suffix = ");"; if (dd.type === "SequenceExpression") { // If the exported expression is a comma-separated sequence // expression, this.code.slice(dd.start, dd.end) may not include // the vital parentheses, so we should wrap the expression with // parentheses to make absolutely sure it is treated as a single // argument to the module.exportDefault method, rather than as // multiple arguments. prefix += "("; suffix = ")" + suffix; } overwrite(this, decl.start, dd.start, prefix); overwrite(this, dd.end, decl.end, suffix, true); if (this.modifyAST) { // A Function or Class declaration has become an expression on the // right side of the exportDefaultPrefix assignment above so // change the AST appropriately if (dd.type === "FunctionDeclaration") { dd.type = "FunctionExpression"; } else if (dd.type === "ClassDeclaration") { dd.type = "ClassExpression"; } // Almost every JS parser parses this expression the same way, but // we should still give custom parsers a chance to parse it. let ast = this.parse(this.moduleAlias + ".exportDefault(ARG);"); if (ast.type === "File") ast = ast.program; assert.strictEqual(ast.type, "Program"); const callExprStmt = ast.body[0]; assert.strictEqual(callExprStmt.type, "ExpressionStatement"); const callExpr = callExprStmt.expression; assert.strictEqual(callExpr.type, "CallExpression"); // Replace the ARG identifier with the exported expression. callExpr.arguments[0] = dd; path.replace(callExprStmt); } this.madeChanges = true; } } visitExportNamedDeclaration(path) { const decl = path.getValue(); const dd = decl.declaration; if (dd) { const specifierMap = Object.create(null); const type = dd.type; if (dd.id && (type === "ClassDeclaration" || type === "FunctionExpression" || type === "FunctionDeclaration")) { addNameToMap(specifierMap, dd.id.name); } else if (type === "VariableDeclaration") { const ddCount = dd.declarations.length; for (let i = 0; i < ddCount; ++i) { const names = utils.getNamesFromPattern(dd.declarations[i].id); const nameCount = names.length; for (let j = 0; j < nameCount; ++j) { addNameToMap(specifierMap, names[j]); } } } hoistExports(this, path, specifierMap, "declaration"); if (canExportedValuesChange(decl)) { // We can skip adding declared names to this.exportedLocalNames if // the declaration was a const-kinded VariableDeclaration, because // the assignmentVisitor will not need to worry about changes to // these variables. addExportedLocalNames(this, specifierMap); } return; } if (decl.specifiers) { let specifierMap = computeSpecifierMap(decl.specifiers); if (decl.source) { if (specifierMap) { const newMap = Object.create(null); const keys = Object.keys(specifierMap); const keyCount = keys.length; for (let i = 0; i < keyCount; ++i) { const exported = keys[i]; const locals = specifierMap[exported]; const localCount = locals.length; for (let j = 0; j < localCount; ++j) { addToSpecifierMap(newMap, locals[j], "exports." + exported); } } specifierMap = newMap; } // Although this is an export declaration, it also imports the // source module, and uses the module.link API, so it should be // hoisted as if it was an import declaration. hoistImports(this, path, toModuleImport( this, getSourceString(this, decl), specifierMap )); } else { hoistExports(this, path, specifierMap); addExportedLocalNames(this, specifierMap); } } } }; // Babel uses Import instead of ImportExpression for the node type ImportExportVisitor.prototype.visitImport = ImportExportVisitor.prototype.visitImportExpression; function addExportedLocalNames(visitor, specifierMap) { const exportedLocalNames = visitor.exportedLocalNames; const keys = Object.keys(specifierMap); const keyCount = keys.length; for (let i = 0; i < keyCount; ++i) { const exported = keys[i]; const locals = specifierMap[exported]; const localCount = locals.length; for (let j = 0; j < localCount; ++j) { if (exportedLocalNames[locals[j]] === undefined) { exportedLocalNames[locals[j]] = []; } exportedLocalNames[locals[j]].push(exported); } } } function addNameToMap(map, name) { addToSpecifierMap(map, name, name); } function addToSpecifierMap(map, __ported, local) { assert.strictEqual(typeof __ported, "string"); assert.strictEqual(typeof local, "string"); const locals = __ported in map ? map[__ported] : []; if (locals.indexOf(local) < 0) { locals.push(local); } map[__ported] = locals; return map; } // Returns a map from {im,ex}ported identifiers to lists of local variable // names bound to those identifiers. function computeSpecifierMap(specifiers) { const specifierCount = specifiers.length; const specifierMap = Object.create(null); for (let i = 0; i < specifierCount; ++i) { const s = specifiers[i]; const local = s.type === "ExportDefaultSpecifier" ? "default" : s.type === "ExportNamespaceSpecifier" ? "*" : s.local.name; const __ported = // The IMported or EXported name. s.type === "ImportSpecifier" ? s.imported.name : s.type === "ImportDefaultSpecifier" ? "default" : s.type === "ImportNamespaceSpecifier" ? "*" : (s.type === "ExportSpecifier" || s.type === "ExportDefaultSpecifier" || s.type === "ExportNamespaceSpecifier") ? s.exported.name : null; if (typeof local === "string" && typeof __ported === "string") { addToSpecifierMap(specifierMap, __ported, local); } } return specifierMap; } function getBlockBodyInfo(visitor, path) { const node = path.getNode(); let parent = path.getParentNode(); if (parent === null) { parent = node; } let body = parent.body; let bodyName = "body"; let insertCharIndex = node.start; let needToAddUseStrictDirective = false; switch (parent.type) { case "Program": insertCharIndex = parent.start; // If parent is a Program, we may need to add a "use strict" // directive to enable const/let in Node 4. needToAddUseStrictDirective = visitor.enforceStrictMode; break; case "BlockStatement": insertCharIndex = parent.start + 1; break; case "SwitchCase": body = parent.consequent; bodyName = "consequent"; insertCharIndex = body[0].start; break; default: const block = { type: "BlockStatement", body: [], start: node.start, end: node.end + 2 }; body = block.body; bodyName = path.getName(); insertCharIndex = node.start; if (visitor.magicString !== null) { visitor.magicString .appendLeft(insertCharIndex, "{") .prependRight(node.end, "}"); } if (visitor.modifyAST) { path.replace(block); } } assert.ok(Array.isArray(body), body); // Avoid hoisting above string literal expression statements such as // "use strict", which may depend on occurring at the beginning of // their enclosing scopes. let insertNodeIndex = 0; const stmtCount = body.length; for (let i = 0; i < stmtCount; ++i) { const stmt = body[i]; if (stmt.type === "ExpressionStatement") { const expr = stmt.expression; if (expr.type === "StringLiteral" || (expr.type === "Literal" && typeof expr.value === "string")) { insertCharIndex = stmt.end; insertNodeIndex = i + 1; if (expr.value === "use strict") { // If there's already a "use strict" directive, then we don't // need to add another one. needToAddUseStrictDirective = false; } continue; } } break; } // Babylon represents directives like "use strict" with a .directives // array property on the parent node. const directives = parent.directives; const directiveCount = directives ? directives.length : 0; for (let i = 0; i < directiveCount; ++i) { const d = directives[i]; insertCharIndex = Math.max(d.end, insertCharIndex); if (d.value.value === "use strict") { // If there's already a "use strict" directive, then we don't // need to add another one. needToAddUseStrictDirective = false; } } let bibn = parent._bodyInfoByName; if (bibn === void 0) { bibn = parent._bodyInfoByName = Object.create(null); } let bodyInfo = bibn[bodyName]; if (bodyInfo === void 0) { bodyInfo = bibn[bodyName] = Object.create(null); bodyInfo.body = body; bodyInfo.parent = parent; bodyInfo.insertCharIndex = insertCharIndex; bodyInfo.insertNodeIndex = insertNodeIndex; bodyInfo.hoistedExportsMap = Object.create(null); bodyInfo.hoistedConstExportsMap = Object.create(null); bodyInfo.hoistedExportsString = ""; bodyInfo.hoistedImportsString = ""; } else { assert.strictEqual(bodyInfo.body, body); } if (visitor.bodyInfos.indexOf(bodyInfo) < 0) { visitor.bodyInfos.push(bodyInfo); } if (needToAddUseStrictDirective) { bodyInfo.needToAddUseStrictDirective = needToAddUseStrictDirective; } return bodyInfo; } // Gets a string representation (including quotes) from an import or // export declaration node. function getSourceString(visitor, decl) { const code = visitor.code; if (code) { return code.slice( decl.source.start, decl.source.end ); } assert.strictEqual(typeof decl.source.value, "string"); return JSON.stringify(decl.source.value); } function hoistImports(visitor, importDeclPath, hoistedCode) { preserveLine(visitor, importDeclPath); const bodyInfo = getBlockBodyInfo(visitor, importDeclPath); bodyInfo.hoistedImportsString += hoistedCode; visitor.madeChanges = true; } function hoistExports(visitor, exportDeclPath, mapOrString, childName) { if (childName) { preserveChild(visitor, exportDeclPath, childName); } else { preserveLine(visitor, exportDeclPath); } const bodyInfo = getBlockBodyInfo(visitor, exportDeclPath); const constant = ! canExportedValuesChange(exportDeclPath.getValue()); if (typeof mapOrString !== "string") { const keys = Object.keys(mapOrString); const keyCount = keys.length; for (let i = 0; i < keyCount; ++i) { const exported = keys[i]; const locals = mapOrString[exported]; const localCount = locals.length; for (let j = 0; j < localCount; ++j) { addToSpecifierMap( constant ? bodyInfo.hoistedConstExportsMap : bodyInfo.hoistedExportsMap, exported, locals[j] ); } } } else { bodyInfo.hoistedExportsString += mapOrString; } visitor.madeChanges = true; } function canExportedValuesChange(exportDecl) { if (exportDecl) { if (exportDecl.type === "ExportDefaultDeclaration") { const dd = exportDecl.declaration; return (dd.type === "FunctionDeclaration" || dd.type === "FunctionExpression" || dd.type === "ClassDeclaration"); } if (exportDecl.type === "ExportNamedDeclaration") { const dd = exportDecl.declaration; if (dd && dd.type === "VariableDeclaration" && dd.kind === "const") { return false; } } } return true; } function makeUniqueKey(visitor) { return visitor.nextKey++; } function overwrite(visitor, oldStart, oldEnd, newCode, trailing) { if (! visitor.code) { return; } assert.strictEqual(typeof oldStart, "number"); assert.strictEqual(typeof oldEnd, "number"); assert.strictEqual(typeof newCode, "string"); const padded = pad(visitor, newCode, oldStart, oldEnd); if (oldStart === oldEnd) { if (padded === "") { return; } if (trailing) { visitor.magicString.appendLeft(oldStart, padded); } else { visitor.magicString.prependRight(oldStart, padded); } } else { visitor.magicString.overwrite(oldStart, oldEnd, padded); } } function pad(visitor, newCode, oldStart, oldEnd) { const code = visitor.code; if (code) { const oldLines = code.slice(oldStart, oldEnd).split("\n"); const oldLineCount = oldLines.length; const newLines = newCode.split("\n"); const lastIndex = newLines.length - 1; for (let i = lastIndex; i < oldLineCount; ++i) { const oldLine = oldLines[i]; const lastCharCode = oldLine.charCodeAt(oldLine.length - 1); if (i > lastIndex) { newLines[i] = ""; } if (lastCharCode === codeOfCR) { newLines[i] += "\r"; } } newCode = newLines.join("\n"); } return newCode; } function preserveChild(visitor, path, childName) { const value = path.getValue(); const child = value ? value[childName] : null; if (child && visitor.code) { overwrite( visitor, value.start, child.start, "" ); overwrite( visitor, child.end, value.end, "" ); } path.call(visitor.visitWithoutReset, childName); if (visitor.modifyAST) { // Replace the given path with the child we want to preserve. path.replace(child); } } function preserveLine(visitor, path) { const value = path.getValue(); if (visitor.code) { overwrite(visitor, value.start, value.end, ""); } if (visitor.modifyAST) { visitor.removals.push({ container: path.getContainer(), name: path.getName(), value }); } } function processRemoval(removal) { if (Array.isArray(removal.container)) { const index = removal.container.indexOf(removal.value); if (index >= 0) { removal.container.splice(index, 1); } } else if (removal.value === removal.container[removal.name]) { // This case is almost certainly never reached. removal.container[removal.name] = null; } else { const newValue = removal.container[removal.name]; if (newValue.type === "BlockStatement") { // This newValue is a BlockStatement that we created in the default // case of the switch statement in getBlockBodyInfo, so we make sure // the original import/export declaration is no longer in its .body. processRemoval({ container: newValue.body, value: removal.value }); } } } function safeKey(key) { if (/^[_$a-zA-Z]\w*$/.test(key)) { return key; } return JSON.stringify(key); } function safeParam(param, locals) { if (locals.indexOf(param) < 0) { return param; } return safeParam("_" + param, locals); } function toModuleImport(visitor, source, specifierMap) { let code = visitor.moduleAlias + ".link(" + source; const importedNames = Object.keys(specifierMap); const nameCount = importedNames.length; if (! nameCount) { code += ");"; return code; } const lastIndex = nameCount - 1; code += ",{"; const properties = []; for (let i = 0; i < nameCount; ++i) { const imported = importedNames[i]; const reexports = []; const locals = []; specifierMap[imported].forEach(local => { if (local.startsWith("exports.")) { var parts = local.split("."); parts.shift(); reexports.push(parts.join(".")); } else { locals.push(local); } }); if (locals.length > 0) { let property = safeKey(imported); if (visitor.avoidModernSyntax) { property += ":function"; } const valueParam = safeParam("v", locals); property += "(" + valueParam + "){"; // Multiple local variables become a compound assignment. property += locals.join("=") + "=" + valueParam, property += "}" properties.push(property); } if (reexports.length > 0) { properties.push( safeKey(imported) + ":" + JSON.stringify( // Avoid wrapping single strings as arrays, for brevity: reexports.length === 1 ? reexports[0] : reexports ) ); } } code += properties.join(",") + "}," + makeUniqueKey(visitor) + ");"; return code; } function toModuleExport(visitor, specifierMap, constant) { let code = ""; const exportedKeys = Object.keys(specifierMap); const keyCount = exportedKeys.length; if (! keyCount) { return code; } const lastIndex = keyCount - 1; code += visitor.moduleAlias + ".export({"; for (let i = 0; i < keyCount; ++i) { const exported = exportedKeys[i]; const isLast = i === lastIndex; const locals = specifierMap[exported]; assert.strictEqual(locals.length, 1); if (visitor.avoidModernSyntax) { code += exported + ":function(){return " + locals[0] + "}"; } else { code += exported + ":()=>" + locals[0]; } if (! isLast) { code += ","; } } // The second argument to module.export indicates whether the getter // functions provided in the first argument are constant or not. code += constant ? "},true);" : "});"; return code; } module.exports = ImportExportVisitor;