UNPKG

cjs-es

Version:

Transform CommonJS module into ES module.

487 lines (468 loc) 15 kB
function createExportWriter(context) { context.defaultExports = []; context.namedExports = new Map; context.objectExports = new Map; context.finalExportType = null; return {write}; function write() { if (!context.moduleNodes.length && !context.exportsNodes.length) { return; } // passing module around if (context.moduleNodes.some(n => !n.exported && !n.nestedExports && !n.declared)) { return bindToSingleModule(); } for (const node of context.moduleNodes.concat(context.exportsNodes)) { const name = node.exported && node.exported.name || node.declared && node.declared.exported.name || node.nestedExports && node.nestedExports.name; // console.log(name); if (!name) { context.defaultExports.push(node); continue; } let nodes = context.namedExports.get(name); if (!nodes) { nodes = []; context.namedExports.set(name, nodes); } nodes.push(node); } // export object literal? if (isObjectMapExport()) { const node = context.defaultExports[0]; const {properties} = node.exported.object; let start = node.exported.leftMost.start; let nameInfo; for (let i = 0; i < properties.length; i++) { properties[i].rootPos = node.rootPos; nameInfo = { type: "objectProperty", start, node: properties[i], newLine: i > 0 }; const name = properties[i].key.name; let infos = context.objectExports.get(name); if (!infos) { infos = []; context.objectExports.set(name, infos); } infos.push(nameInfo); start = properties[i].value.end; } nameInfo.trim = [start, node.exported.statement.end]; } // sometimes it's impossible to use named exports if ( context.defaultExports.length > 1 || context.defaultExports.length === 1 && ( !isObjectMapExport() || context.hasDefaultComment(context.defaultExports[0]) ) ) { return bindToSingleExport(); } // export named return Promise.resolve(context.isExportPreferDefault()) .then(preferDefault => { if (preferDefault) { return bindToSingleExport(); } return bindToNames(); }); } function isSingleLineExport() { if (context.defaultExports.length !== 1) { return false; } const node = context.defaultExports[0]; return node.exported && node.exported.statement || node.declared; } function isObjectMapExport() { if (!isSingleLineExport()) { return false; } const node = context.defaultExports[0]; if (node.declared || !node.exported.object) { // bind to a single reference return false; } const props = node.exported.object.properties; if (props.some(p => p.value.containThis)) { return false; } // some names are not exported in the object // FIXME: is this the correct behavior? const keys = new Set(props.map(p => p.key.name)); for (const k of context.namedExports.keys()) { if (!keys.has(k)) { return false; } } return true; } function bindToSingleModule() { if ( context.moduleNodes.length && context.exportsNodes.length && context.exportsNodes[0].rootPos > context.moduleNodes[0].rootPos ) { context.exportsNodes[0].rootPos = context.moduleNodes[0].rootPos; } if (context.exportsNodes.length) { context.s.appendLeft( context.exportsNodes[0].rootPos, "let _exports_ = {};\n" ); } if (context.moduleNodes.length) { context.s.appendLeft( context.moduleNodes[0].rootPos, `const _module_ = {exports: ${context.exportsNodes.length ? "_exports_" : "{}"}};\n` ); } for (const node of context.moduleNodes) { context.s.overwrite(node.start, node.end, "_module_", {contentOnly: true}); } for (const node of context.exportsNodes) { context.s.overwrite(node.start, node.end, "_exports_", {contentOnly: true}); } if (context.moduleNodes.length) { context.s.appendLeft( context.topLevel.get().end, "\nexport default _module_.exports;" ); } else { context.s.appendLeft( context.topLevel.get().end, "\nexport {_exports_ as default};" ); } context.finalExportType = "default"; } function bindToSingleLineExport() { const node = context.defaultExports[0]; if (node.declared) { // const foo = module.exports = ... context.s.overwrite( node.declared.id.end, node.declared.exported.value.start, " = " ); context.s.remove( node.declared.exported.value.end, node.declared.end ); context.s.appendLeft( node.declared.end, `;\nexport {${node.declared.id.name} as default};` ); } else { // module.exports = ... context.s.overwrite( node.exported.leftMost.start, node.exported.value.start, "export default " ); if ( node.exported.value.type === "FunctionExpression" || node.exported.value.type === "ClassExpression" ) { // convert declaration into expression if (node.exported.value.id) { context.s.appendLeft(node.exported.value.start, "("); context.safeOverwrite(node.exported.value.end, node.exported.statement.end, ");"); } else { context.s.remove(node.exported.value.end, node.exported.statement.end); } } else { context.safeOverwrite(node.exported.value.end, node.exported.statement.end, ";"); } if (node.exported.isIife && context.code[node.exported.value.start] !== "(") { // wrap iife expression context.s.appendRight(node.exported.value.callee.start, "("); context.s.appendLeft(node.exported.value.callee.end, ")"); } } context.finalExportType = "default"; } function writeProperty({ start, node: property, newLine, assignOnly = false, shareExport = false, kind = "const", trim }) { if (newLine) { context.s.appendLeft(start, "\n"); } const valuePrefix = property.method ? `function${property.generator ? "*" : ""} ` : ""; if (assignOnly) { context.s.overwrite( start, property.value.start, `_export_${property.key.name}_ = ${valuePrefix}`, {contentOnly: true} ); } else if (!shareExport && (property.value.type === "Identifier" || property.required)) { // foo: bar context.s.overwrite( start, property.value.start, "export {", {contentOnly: true} // don't overwrite previous }; ); context.s.appendLeft( property.value.end, `${property.key.name !== property.value.name ? ` as ${property.key.name}` : ""}}` ); } else { // foo: "not an identifier" context.s.overwrite( start, property.value.start, `${kind} _export_${property.key.name}_ = ${valuePrefix}`, {contentOnly: true} ); context.s.appendLeft( property.value.end, `;\nexport {_export_${property.key.name}_ as ${property.key.name}}` ); } context.s.appendLeft(property.value.end, ";"); if (trim) { context.s.remove(trim[0], trim[1]); } } function bindToNames() { // merge namedExports and objectExports const allExports = new Map; for (const [name, nodes] of context.namedExports) { if (!allExports.has(name)) { allExports.set(name, []); } allExports.get(name).push(...nodes.map(n => ({node: n, type: "name"}))); } for (const [name, infos] of context.objectExports) { if (!allExports.has(name)) { allExports.set(name, []); } allExports.get(name).push(...infos); } // names for (const [name, infos] of allExports) { let init = 0; let assignment = 0; // let declared = 0; for (const info of infos) { if (info.type === "name") { const node = info.node; if (node.declared) { // declared++; init++; assignment++; } else if (node.exported) { if (node.exported.statement) { init++; } assignment++; } else if (node.nestedExports.node.isAssignment) { assignment++; } } else { init++; assignment++; } } // FIXME: find a way to detect if the init is the first access to the variable? if (init === 1 && infos.length === 1) { for (const info of infos) { if (info.type === "name") { const node = info.node; if (node.declared) { writeNamedDeclare(node); } else if (node.exported && node.exported.statement) { writeNamedExports(node, assignment > 1 ? "let" : "const", infos.length > 1); } else { writeNestedExports(node); } } else { info.kind = assignment > 1 ? "let" : "const"; info.shareExport = infos.length > 1; writeProperty(info); } } } else { for (const info of infos) { if (info.type === "name") { const node = info.node; if (node.declared) { node.exported = node.declared.exported; } writeNestedExports(node); } else { info.assignOnly = true; writeProperty(info); } } const poses = infos.map(i => i.node.rootPos); writeNamedInit(Math.min(...poses), name); } } context.finalExportType = "named"; } function writeNamedDeclare(node) { if (node.declared.id.name === node.declared.exported.name) { context.s.overwrite( node.declared.start, node.declared.exported.key.start, `export ${node.declared.kind} ` ); } else { context.s.overwrite( node.declared.id.end, node.declared.exported.value.start, " = " ); context.s.remove( node.declared.exported.value.end, node.declared.end ); context.s.appendLeft( node.declared.end, `;\nexport {${node.declared.id.name} as ${node.declared.exported.name}};` ); } } function writeNamedExports(node, kind = "const", shareExport = false) { if (node.exported.value.type !== "Identifier" && !node.exported.required || shareExport) { context.s.overwrite( node.exported.leftMost.start, node.exported.value.start, `${kind} _export_${node.exported.name}_ = `, {contentOnly: true} ); context.safeOverwrite( node.exported.value.end, node.exported.statement.end, `;\nexport {_export_${node.exported.name}_ as ${node.exported.name}};` ); } else { context.s.overwrite( node.exported.leftMost.start, node.exported.value.start, "export {", {contentOnly: true} ); context.safeOverwrite( node.exported.value.end, node.exported.statement.end, `${node.exported.value.name !== node.exported.name ? ` as ${node.exported.name}` : ""}};` ); } } function writeNestedExports(node) { const target = node.nestedExports || node.exported; context.s.overwrite( target.node.start, target.node.end, `_export_${target.name}_`, {contentOnly: true} ); } function writeNamedInit(node, name) { context.s.appendLeft( node.rootPos, `let _export_${name}_;\nexport {_export_${name}_ as ${name}};\n` ); } function bindToSingleExport() { if (isSingleLineExport() && !context.namedExports.size) { return bindToSingleLineExport(); } const nodes = context.moduleNodes.concat(context.exportsNodes); let init = 0; let assignment = 0; let childAssignment = 0; for (const node of nodes) { if (node.exported) { if (node.exported.name) { continue; } if (node.exported.statement) { init++; } if (!node.parentAssign) { assignment++; } else { childAssignment++; } } else if ( node.nestedExports && node.nestedExports.node.isAssignment || node.isAssignment ) { assignment++; } } // FIXME: find a way to detect whether the init statement is the first access to the variable // so we can also use writeDefaultExportDeclare when nodes.length > 1 // https://github.com/eight04/cjs-es/issues/28 if (init === 1 && nodes.length === 1) { for (const node of nodes) { if (node.exported && node.exported.statement && !node.exported.name) { writeDefaultExportDeclare(node, assignment === 1 ? "const" : "let"); } else { writeDefaultExportNode(node); } } } else { // NOTE: we can't actually use topIndex... the script may access _module_exports_ indirectly through function call. // const topIndex = nodes.reduce( // (r, n) => n.rootPos < r ? n.rootPos : r, // Infinity // ); const kind = assignment ? "let" : "const"; const defaultValue = assignment + childAssignment === nodes.length && !context.needDefaultObject ? "" : " = {}"; context.s.appendLeft( 0, `${kind} _module_exports_${defaultValue};\nexport {_module_exports_ as default};\n` ); for (const node of nodes) { writeDefaultExportNode(node); } } context.finalExportType = "default"; } function writeDefaultExportDeclare(node, kind) { const target = node.childAssign || node; context.s.overwrite( node.exported.node.start, target.exported.assignExpression.left.end, `${kind} _module_exports_`, {contentOnly: true} ); context.s.appendLeft(node.exported.statement.end, "\nexport {_module_exports_ as default};"); } function writeDefaultExportNode(node) { if (node.parentAssign && node.parentAssign.childAssign) { return; // ignore module.exports = exports = ... } let start, end; if (node.childAssign) { start = node.start; end = node.childAssign.exported.assignExpression.left.end; } else { const exported = node.exported || node.nestedExports; const target = exported ? (exported.name ? exported.node.object : exported.node) : node; start = target.start; end = target.end; } context.s.overwrite(start, end, "_module_exports_", {contentOnly: true}); } } module.exports = {createExportWriter};