UNPKG

moduloze

Version:

Convert CommonJS (CJS) modules to UMD and ESM formats

758 lines (702 loc) 22.5 kB
"use strict"; var { default: traverse, } = require("@babel/traverse"); var T = require("@babel/types"); var { parse, } = require("@babel/parser"); var { findParentStatement, isFunction, isAssignmentTarget, } = require("./helpers.js"); module.exports.identifyRequiresAndExports = identifyRequiresAndExports; module.exports.analyzeRequires = analyzeRequires; module.exports.analyzeExports = analyzeExports; // ****************************** function identifyRequiresAndExports(codePath,code) { var programPath; var requireStatements = new Set(); var exportStatements = new Set(); var requireCalls = new WeakMap(); var exportReferences = new WeakMap(); var visitors = { Program: { exit(path) { programPath = path; }, }, CallExpression: { exit(path) { // require(..) call? if (T.isIdentifier(path.node.callee,{ name: "require", })) { // require(" some string literal ") ? if ( path.node.arguments.length == 1 && T.isStringLiteral(path.node.arguments[0]) ) { let parentStatementPath = findParentStatement(path.parentPath); if (parentStatementPath) { requireStatements.add(parentStatementPath); if (!requireCalls.has(parentStatementPath)) { requireCalls.set(parentStatementPath,[]); } requireCalls.get(parentStatementPath).push(path); } } // non-string literals not supported else { throw new Error("Unsupported: require(..) statement without a single string-literal argument"); } } } }, MemberExpression: { exit(path) { // module.exports? if (isModuleExports(path.node)) { let parentStatementPath = findParentStatement(path.parentPath); if (parentStatementPath) { exportStatements.add(parentStatementPath); if (!exportReferences.has(parentStatementPath)) { exportReferences.set(parentStatementPath,{ type: ( isAssignmentTarget(path) ? "assignment" : "expression" ), refs: [], }); } exportReferences.get(parentStatementPath).refs.push(path); } } } }, Identifier: { exit(path) { // exports? if ( path.node.name == "exports" && // not part of a member expression? (intentionally excludes module.exports) !T.isMemberExpression(path.parent,{ property: path.node, }) && ( // in a left-hand assignment target postion? isAssignmentTarget(path) || ( // not a function parameter !isFunction(path.parentPath) && // not a property in an object literal !T.isObjectProperty(path.parent,{ key: path.node, }) ) ) ) { let parentStatementPath = findParentStatement(path.parentPath); if (parentStatementPath) { exportStatements.add(parentStatementPath); if (!exportReferences.has(parentStatementPath)) { exportReferences.set(parentStatementPath,{ type: ( isAssignmentTarget(path) ? "assignment" : "expression" ), refs: [], }); } exportReferences.get(parentStatementPath).refs.push(path); } } } } }; var programAST = parse(code,{ sourceFilename: codePath, }); traverse(programAST,visitors); var convertRequires = analyzeRequires(requireStatements,requireCalls); var convertExports = analyzeExports(exportStatements,exportReferences); return { programAST, programPath, convertRequires, convertExports, }; } function analyzeRequires(requireStatements,requireCalls) { var convertRequires = []; for (let stmtPath of requireStatements) { if (!T.isProgram(stmtPath.parent)) { throw new Error("Require statements must be at the top-level of the program"); } let stmtReqCalls = [ ...requireCalls.get(stmtPath), ]; // standalone require(".."")? if ( T.isExpressionStatement(stmtPath.node) && T.isCallExpression(stmtPath.node.expression) && stmtReqCalls.length == 1 && stmtReqCalls[0].node == stmtPath.node.expression ) { // unset entry to mark this require(..) expression as handled stmtReqCalls[0] = false; let call = stmtPath.node.expression; let specifier = call.arguments[0].extra.rawValue; // console.log(`import ${ specifier };`); convertRequires.push({ esmType: "bare-import", umdType: "remove-require-unique", specifier, context: { statement: stmtPath, }, }); } // var/let/const declaration statement? else if (T.isVariableDeclaration(stmtPath.node)) { for (let [ declIdx, declNode, ] of stmtPath.node.declarations.entries()) { // no require(..) in this declaration, so skip? if (!hasRequireCalls(stmtPath,declNode.init,stmtReqCalls)) { continue; } let decl = stmtPath.get(`declarations.${ declIdx }`); // normal identifier declaration? var x = .. if (T.isIdentifier(declNode.id)) { // call as initialization assignment? var x = require(..) if (T.isCallExpression(declNode.init) && stmtReqCalls.find(p => p.node == declNode.init) ) { // unset entry to mark this require(..) expression as handled stmtReqCalls[stmtReqCalls.findIndex(p => p.node == declNode.init)] = false; let callPath = decl.get("init"); let call = callPath.node; let specifier = call.arguments[0].extra.rawValue; // console.log(`import * as ${ declNode.id.name } from ${ specifier };`); // console.log(`import ${ declNode.id.name } from ${ specifier };`); convertRequires.push({ esmType: "default-import", umdType: "default-require", binding: { target: declNode.id.name }, specifier, context: { statement: stmtPath, declarator: decl, declarationIdx: declIdx, requireCall: callPath, }, }); } else if ( // require(..) is part of a simple member expression? isSimpleMemberExpression(declNode.init,declNode) && stmtReqCalls.find(p => p.node == declNode.init.object) ) { // unset entry to mark this require(..) expression as handled stmtReqCalls[stmtReqCalls.findIndex(p => p.node == declNode.init.object)] = false; let callPath = decl.get("init.object"); let call = callPath.node; let specifier = call.arguments[0].extra.rawValue; let target = declNode.id.name; let source = T.isIdentifier(declNode.init.property) ? declNode.init.property.name : T.isStringLiteral(declNode.init.property) ? declNode.init.property.value : undefined; // console.log(`import { ${ binding } } from ${ specifier };`); convertRequires.push({ esmType: "named-import", umdType: "named-dependency", binding: { source, target, }, specifier, context: { statement: stmtPath, declarator: decl, declarationIdx: declIdx, requireCall: callPath, }, }); } } // destructuring assignment? var { x } = require(..) else if ( T.isObjectPattern(declNode.id) && T.isCallExpression(declNode.init) && stmtReqCalls.find(p => p.node == declNode.init) ) { // unset entry to mark this require(..) expression as handled stmtReqCalls[stmtReqCalls.findIndex(p => p.node == declNode.init)] = false; let callPath = decl.get("init"); let call = callPath.node; let specifier = call.arguments[0].extra.rawValue; let pattern = declNode.id; let bindings = []; for (let targetProp of pattern.properties) { // simple destructuring target? if ( !targetProp.computed && T.isIdentifier(targetProp.value) ) { let source = T.isIdentifier(targetProp.key) ? targetProp.key.name : T.isStringLiteral(targetProp.key) ? targetProp.key.value : undefined; bindings.push({ source, target: targetProp.value.name, }); continue; } // if we get here, the destructuring wasn't of a supported ESM import form throw new Error("Unsupported: destructuring pattern not ESM import-compatible"); } // console.log(`import { ${ binding } } from ${ specifier };`); convertRequires.push({ esmType: "named-import", umdType: "destructured-dependency", binding: bindings, specifier, context: { statement: stmtPath, declarator: decl, declarationIdx: declIdx, requireCall: callPath, }, }); } } } // non-declaration assignment statement? else if ( T.isExpressionStatement(stmtPath.node) && T.isAssignmentExpression(stmtPath.node.expression) ) { let assignment = stmtPath.node.expression; // regular identifier assignment? x = .. if (T.isIdentifier(assignment.left)) { // simple call assignment? x = require(..) if (stmtReqCalls.find(p => p.node == assignment.right)) { // unset entry to mark this require(..) expression as handled stmtReqCalls[stmtReqCalls.findIndex(p => p.node == assignment.right)] = false; let callPath = stmtPath.get("expression.right"); let call = callPath.node; let specifier = call.arguments[0].extra.rawValue; let target = assignment.left.name; // console.log(`import * as ${ target$1 } from ${ specifier }; ${ target } = ${ target$1 };`); // console.log(`import ${ target$1 } from ${ specifier }; ${ target } = ${ target$1 };`); convertRequires.push({ esmType: "default-import-indirect", umdType: "indirect-target", binding: { source: "default", target, uniqueTarget: stmtPath.scope.generateUidIdentifier("imp").name, }, specifier, context: { statement: stmtPath, requireCall: callPath, }, }); } else if ( // require(..) part of a simple member expression? isSimpleMemberExpression(assignment.right,assignment) && stmtReqCalls.find(p => p.node == assignment.right.object) ) { // unset entry to mark this require(..) expression as handled stmtReqCalls[stmtReqCalls.findIndex(p => p.node == assignment.right.object)] = false; let callPath = stmtPath.get("expression.right.object"); let call = callPath.node; let specifier = call.arguments[0].extra.rawValue; let target = assignment.left.name; let source = T.isIdentifier(assignment.right.property) ? assignment.right.property.name : T.isStringLiteral(assignment.right.property) ? assignment.right.property.value : undefined; // console.log(`import { ${ binding } } from ${ specifier }; ${ target } = ${ target$1 };`); convertRequires.push({ esmType: "named-import-indirect", umdType: "indirect-source-target", binding: { source, target, uniqueTarget: stmtPath.scope.generateUidIdentifier("imp").name, }, specifier, context: { statement: stmtPath, requireCall: callPath, }, }); } } // destructuring assignment? { x } = require(..) else if ( T.isObjectPattern(assignment.left) && stmtReqCalls.find(p => p.node == assignment.right) ) { // unset entry to mark this require(..) expression as handled stmtReqCalls[stmtReqCalls.findIndex(p => p.node == assignment.right)] = false; let callPath = stmtPath.get("expression.right"); let call = callPath.node; let specifier = call.arguments[0].extra.rawValue; let pattern = assignment.left; let bindings = []; for (let targetProp of pattern.properties) { // simple destructuring target? if ( !targetProp.computed && T.isIdentifier(targetProp.value) ) { let source = T.isIdentifier(targetProp.key) ? targetProp.key.name : T.isStringLiteral(targetProp.key) ? targetProp.key.value : undefined; bindings.push({ source, target: targetProp.value.name, uniqueTarget: stmtPath.scope.generateUidIdentifier("imp").name, }); continue; } // if we get here, the `require(..)` wasn't of a supported form throw new Error("Unsupported: destructuring pattern not ESM import-compatible"); } if (bindings.length > 0) { // console.log(`import { ${ binding } } from ${ specifier }; ${ target } = ${ target$1 };`); convertRequires.push({ esmType: "named-import-indirect", umdType: "indirect-source-target", binding: bindings, specifier, context: { statement: stmtPath, requireCall: callPath, }, }); } } // default or named re-export? (aka, "combo") // ie, module.exports = require(..).. OR module.exports.x = require(..).. else if ( isModuleExports(assignment.left) || ( isSimpleMemberExpression(assignment.left,assignment) && isModuleExports(assignment.left.object) ) ) { let target = assignment.left; // require(..) by itself? if (stmtReqCalls.find(p => p.node == assignment.right)) { // unset entry to mark this require(..) expression as handled stmtReqCalls[stmtReqCalls.findIndex(p => p.node == assignment.right)] = false; let callPath = stmtPath.get("expression.right"); let call = callPath.node; let specifier = call.arguments[0].extra.rawValue; // console.log(`import * as ${ target$1 } from ${ specifier }; ${ target } = ${ target$1 };`); // console.log(`import ${ target$1 } from ${ specifier }; ${ target } = ${ target$1 };`); convertRequires.push({ esmType: "default-import-indirect", umdType: "indirect-target", binding: { source: "default", target, uniqueTarget: stmtPath.scope.generateUidIdentifier("imp").name, }, specifier, context: { statement: stmtPath, requireCall: callPath, }, }); } // require(..).x form? else if ( isSimpleMemberExpression(assignment.right,assignment) && stmtReqCalls.find(p => p.node == assignment.right.object) ) { // unset entry to mark this require(..) expression as handled stmtReqCalls[stmtReqCalls.findIndex(p => p.node == assignment.right.object)] = false; let callPath = stmtPath.get("expression.right.object"); let call = callPath.node; let specifier = call.arguments[0].extra.rawValue; let source = T.isIdentifier(assignment.right.property) ? assignment.right.property.name : T.isStringLiteral(assignment.right.property) ? assignment.right.property.value : undefined; // console.log(`import { ${ binding } } from ${ specifier }; ${ target } = ${ target$1 };`); convertRequires.push({ esmType: "named-import-indirect", umdType: "indirect-source-target", binding: { source, target, uniqueTarget: stmtPath.scope.generateUidIdentifier("imp").name, }, specifier, context: { statement: stmtPath, requireCall: callPath, }, }); } } } // remove entries marked as handled stmtReqCalls = stmtReqCalls.filter(Boolean); // any unhandled require(..) occurences in this statement? // handle them as simple expression substitutions if (stmtReqCalls.length > 0) { convertRequires = [ ...convertRequires, ...stmtReqCalls.map(function handleReqCall(reqCallPath){ return analyzeRequireSubstitutions(stmtPath,reqCallPath); }) ]; } } return convertRequires; } function analyzeRequireSubstitutions(stmtPath,callPath) { var specifier = callPath.node.arguments[0].extra.rawValue; // require(..).x form? if ( isSimpleMemberExpression(callPath.parent,callPath.parentPath.parent) ) { let source = T.isIdentifier(callPath.parent.property) ? callPath.parent.property.name : T.isStringLiteral(callPath.parent.property) ? callPath.parent.property.value : undefined; // console.log(`import { ${ binding } } from ${ specifier }; ${ target } = ${ target$1 };`); return { esmType: "substitute-named-import-indirect", umdType: "substitute-indirect-source-target", binding: { source, uniqueTarget: stmtPath.scope.generateUidIdentifier("imp").name, }, specifier, context: { statement: stmtPath, requireCall: callPath, expression: callPath.parentPath, }, }; } // assume just simple require(..) form else { // console.log(`import * as ${ target$1 } from ${ specifier }; ${ target } = ${ target$1 };`); // console.log(`import ${ target$1 } from ${ specifier }; ${ target } = ${ target$1 };`); return { esmType: "substitute-default-import-indirect", umdType: "substitute-indirect-target", binding: { source: "default", uniqueTarget: stmtPath.scope.generateUidIdentifier("imp").name, }, specifier, context: { statement: stmtPath, requireCall: callPath, }, }; } } function analyzeExports(exportStatements,exportReferences) { var convertExports = []; for (let stmtPath of exportStatements) { if (!T.isProgram(stmtPath.parent)) { throw new Error("Exports expressions must be at the top-level of the program"); } let stmtExportExpressions = exportReferences.get(stmtPath); let exprRefs = stmtExportExpressions.refs; if (stmtExportExpressions.type == "assignment") { // single export assignment? if ( T.isExpressionStatement(stmtPath.node) && T.isAssignmentExpression(stmtPath.node.expression) && exprRefs.length == 1 ) { let assignment = stmtPath.node.expression; let target = assignment.left; let source = assignment.right; // assigning to `exports` or `module.exports`? if (target == exprRefs[0].node) { // exporting an identifier? if ( T.isIdentifier(source) && source.name != "undefined" ) { // console.log(`export default ${ source.name };`); convertExports.push({ esmType: "default-export", umdType: "default-assignment", binding: { source, }, context: { statement: stmtPath, exportsExpression: stmtPath.get("expression.left"), }, }); continue; } // exporting any other value/expression else { // console.log("export default ..;"); convertExports.push({ esmType: "default-export", umdType: "default-assignment", binding: { source, }, context: { statement: stmtPath, exportsExpression: stmtPath.get("expression.left"), }, }); continue; } } // assigning to property on module.exports? module.exports.x = .. else if ( T.isMemberExpression(target,{ object: exprRefs[0].node, }) && isSimpleMemberExpression(target,assignment) ) { let exportName = T.isIdentifier(target.property) ? target.property.name : T.isStringLiteral(target.property) ? target.property.value : undefined; // exporting an identifier? if ( T.isIdentifier(source) && source.name != "undefined" ) { // console.log(`export { ${ source.name } as ${ exportName } };`); convertExports.push({ esmType: "named-export", umdType: "named-export", binding: { source: source.name, target: exportName, }, context: { statement: stmtPath, exportsExpression: stmtPath.get("expression.left.object"), }, }); continue; } // exporting member-expression that can be destructured? else if (isSimpleMemberExpression(source,assignment)) { let sourceName = ( T.isIdentifier(source.property) ? source.property.name : T.isStringLiteral(source.property) ? source.property.value : undefined ); // console.log(`export var { ${ sourceName }: ${ exportName } } = ${ source.object }`); convertExports.push({ esmType: "destructured-declaration-export", umdType: "named-export", binding: { sourceName, source: source.object, target: exportName, }, context: { statement: stmtPath, exportsExpression: stmtPath.get("expression.left.object"), }, }); continue; } // exporting any other value/expression else { // console.log(`var ${ exportName }$1 = ..; export { ${exportName}$1 as ${ exportName } };`); convertExports.push({ esmType: "named-declaration-export", umdType: "named-export", binding: { source, target: exportName, uniqueTarget: stmtPath.scope.generateUidIdentifier("exp").name, }, context: { statement: stmtPath, exportsExpression: stmtPath.get("expression.left.object"), }, }); continue; } } } } else if (stmtExportExpressions.type == "expression") { for (let ref of exprRefs) { convertExports.push({ esmType: "substitute-module-exports-reference", umdType: "substitute-module-exports-reference", context: { statement: stmtPath, exportsExpression: ref, }, }); } continue; } // if we get here, the module.exports expression wasn't of a supported form throw new Error("Unsupported: exports expression not ESM export-compatible"); } return convertExports; } function hasRequireCalls(stmtPath,node,stmtReqCalls) { for (let reqCall of stmtReqCalls) { let curPath = reqCall; while (curPath && curPath != stmtPath) { if (curPath.node == node) return true; curPath = curPath.parentPath; } } return false; } function isModuleExports(node) { return ( T.isIdentifier(node,{ name: "exports", }) || ( T.isMemberExpression(node) && T.isIdentifier(node.object,{ name: "module", }) && ( // single property expression via . operator? module.exports ( !node.computed && T.isIdentifier(node.property,{ name: "exports", }) ) || // single property expression via [".."] operator? x["y"] ( node.computed && T.isStringLiteral(node.property,{ value: "exports", }) ) ) ) ); } function isSimpleMemberExpression(node,parentNode) { return ( T.isMemberExpression(node) && ( // single property expression via . operator? x.y ( !node.computed && T.isIdentifier(node.property) ) || // single property expression via [".."] operator? x["y"] ( node.computed && T.isStringLiteral(node.property) ) ) && !T.isCallExpression(parentNode) ); }