esm-to-cjs
Version:
Transform ESM to Common JS for present NodeJS, without any junk wrappers or useless renaming
397 lines (344 loc) • 11.2 kB
JavaScript
module.exports.runTransform = runTransform;
// gives parsing errors better description
let LOOKING_FOR;
// length/distance defaults during parsing
const DISTANCE = 6;
const defaultOptions = {
quote: "double",
lenDestructure: 60,
lenModuleName: 20,
lenIdentifier: 20,
indent: 2
};
function runTransform(str, options = {}) {
options = { ...defaultOptions, ...options };
options.quote = options.quote === "single" ? "'" : '"';
const buffer = [];
const exportBuffer = {
items: [],
requires: []
};
let pos = 0;
for (const token of tokenize(str, options)) {
buffer.push(str.slice(pos, token.start));
buffer.push(transform(token, str, exportBuffer, options));
pos = token.end + 1;
}
// add rest of input
pos = skipNewLines(str, pos);
buffer.push(str.slice(pos, str.length));
if (exportBuffer.items.length) {
const indent = " ".repeat(options.indent);
// add module.exports
for (const item of exportBuffer.requires) {
buffer.push(item);
}
buffer.push("\nmodule.exports = {\n");
const exportNames = exportBuffer.items.map(
item => `${indent}${item[0]}${item[1] ? `: ${item[1]}` : ""}`
);
buffer.push(exportNames.join(",\n"));
buffer.push("\n}");
}
buffer.push("\n"); // end file
return buffer.join("");
}
function transform(token, str, exportBuffer, { indent }) {
indent = " ".repeat(indent);
const { type } = token;
switch (type) {
case "import": {
const identifiers = token.modules.map(s => s.join(": ")).join(", ");
return `const { ${identifiers} } = require(${token.moduleName})`;
}
case "import*":
case "importDefault": {
const { identifier, moduleName } = token;
return `const ${identifier} = require(${moduleName})${
token.isDefaultImport ? ".default" : ""
}`;
}
case "awaitImport": {
return `require(${token.moduleName})`;
}
case "export": {
exportBuffer.items.push(token.modules);
return "";
}
case "reExport": {
const { moduleName } = token;
if (token.modules.length === 1) {
const [original, alias] = token.modules[0];
exportBuffer.items.push([
alias ? alias : original,
`require(${moduleName}).${original}`
]);
return;
}
exportBuffer.requires.push("const {\n");
const names = token.modules
.map(([original]) => `${indent}${original}: __${original}__`)
.join(",\n");
exportBuffer.requires.push(names);
exportBuffer.requires.push(`\n} = require(${moduleName});`);
for (const [original, alias] of token.modules) {
exportBuffer.items.push([alias ? alias : original, `__${original}__`]);
}
return "";
}
case "reExportImported": {
exportBuffer.items.push(...token.modules);
return "";
}
default:
throw new Error("should not reach here");
}
}
String.prototype.indexWithin = indexWithin;
function* tokenize(str, options) {
const { quote, lenDestructure, lenModuleName, lenIdentifier } = options;
let start = 0;
let pos;
const types = new Map([
["import", "import "],
["export", "export "],
["awaitImport", "await import("]
]);
while (types.size !== 0) {
// look for first matching pattern
pos = Number.POSITIVE_INFINITY;
let type;
for (const t of types.keys()) {
const idx = str.indexOf(types.get(t), start);
if (idx === -1) {
types.delete(t);
} else if (idx < pos) {
pos = idx;
type = t;
}
}
switch (type) {
case "import":
yield handleImport();
break;
case "export":
yield handleExport();
break;
case "awaitImport":
yield handleAwaitImport();
break;
}
}
// import { ... } from "MODULE"
// import * as IDENTIFIER from "MODULE"
function handleImport() {
LOOKING_FOR = "import names";
const braceStart = str.indexWithin("{", pos + 7, DISTANCE, false);
// 7 === "import ".length
if (braceStart === -1) {
// try to see if it's `import *`
return handleImportStar();
}
const braceEnd = str.indexWithin("}", braceStart + 1, lenDestructure);
LOOKING_FOR = "name of imported module";
let moduleStart = str.indexWithin("from ", braceEnd + 1, DISTANCE);
moduleStart = str.indexWithin(quote, moduleStart + 1, 5);
const moduleEnd = str.indexWithin(quote, moduleStart + 1, lenModuleName);
start = moduleEnd + 1;
return {
type: "import",
start: pos,
end: moduleEnd,
modules: destructureModules(str.slice(braceStart, braceEnd + 1)),
moduleName: str.slice(moduleStart, moduleEnd + 1)
};
}
// await import(...)
function handleAwaitImport() {
LOOKING_FOR = "name of imported module for await import()";
const moduleStart = str.indexWithin("(", pos + 12, 10) + 1;
// 12 === "await import".length
const moduleEnd = str.indexWithin(")", moduleStart + 1, lenIdentifier) - 1;
start = moduleEnd + 2;
return {
type: "awaitImport",
start: pos,
end: moduleEnd + 1,
moduleName: str.slice(moduleStart, moduleEnd + 1)
};
}
// export [default] const IDENTIFIER = ...
// export [default] function IDENTIFIER(...) ...
// export [default] class IDENTIFIER ...
// export { ... } >> handleReExport()
// export { ... } from "MODULE" >> handleReExport()
function handleExport() {
LOOKING_FOR = "export pattern";
let skipStart = pos + "export ".length;
if (str.indexWithin("{", skipStart, 5, false) !== -1) {
return handleReExport();
} else if (str.indexWithin("*", skipStart, 5, false) !== -1) {
return handleExportStar();
}
LOOKING_FOR = "identifier type (function|class|const) for export";
if (str.indexWithin("async ", skipStart, DISTANCE, false) !== -1) {
skipStart += 6; // 6 === "async ".length;
}
let isDefaultExport = false;
if (str.indexWithin("default ", skipStart, DISTANCE, false) !== -1) {
skipStart += 8; // 8 === "default ".length;
isDefaultExport = true;
}
const typeEnd = str.indexWithin(" ", skipStart, 9);
// 9 === "function".length + 1
const exportType = str.slice(skipStart, typeEnd);
LOOKING_FOR = "export identifiers";
const identifierStart =
str.indexWithin(" ", skipStart + exportType.length, 5) + 1;
const identifierEnd =
str.indexWithin(
exportType === "function" ? "(" : " ",
identifierStart,
lenIdentifier
) - 1;
const end = pos + 6 + (isDefaultExport ? 8 : 0);
// 6 == "export".length, 8 === "default ".length;
const modules = isDefaultExport
? ["default", str.slice(identifierStart, identifierEnd + 1)]
: [str.slice(identifierStart, identifierEnd + 1)];
start = end + 1;
return {
type: "export",
start: pos,
end,
modules
};
}
// import * as IDENTIFIER from "MODULE"
function handleImportStar() {
LOOKING_FOR = "import name for import*";
let identifierStart = str.indexWithin("* as ", pos + 7, DISTANCE, false);
if (identifierStart === -1) {
return handleDefaultImport();
}
identifierStart += 5;
// 7 === "import ".length, 5 === "* as ".length
const identifierEnd = str.indexWithin(
" ",
identifierStart + 1,
lenIdentifier
);
LOOKING_FOR = "name of imported module for import*";
let moduleStart =
str.indexWithin("from ", identifierEnd + 1) + "from".length;
moduleStart = str.indexWithin(quote, moduleStart + 1);
const moduleEnd = str.indexWithin(quote, moduleStart + 1, lenModuleName);
start = moduleEnd + 1;
return {
type: "import*",
start: pos,
end: moduleEnd,
identifier: str.slice(identifierStart, identifierEnd),
moduleName: str.slice(moduleStart, moduleEnd + 1)
};
}
// import IDENTIFIER from "MODULE"
function handleDefaultImport() {
LOOKING_FOR = "import name for default import";
const identifierStart = pos + 7; // 7 === "import ".length
const identifierEnd = str.indexWithin(" ", identifierStart, DISTANCE);
LOOKING_FOR = "name of imported module for import*";
let moduleStart =
str.indexWithin("from ", identifierEnd + 1) + "from".length;
moduleStart = str.indexWithin(quote, moduleStart + 1);
const moduleEnd = str.indexWithin(quote, moduleStart + 1, lenModuleName);
start = moduleEnd + 1;
return {
type: "importDefault",
start: pos,
end: moduleEnd,
isDefaultImport: true,
identifier: str.slice(identifierStart, identifierEnd),
moduleName: str.slice(moduleStart, moduleEnd + 1)
};
}
// export { ... } from "..." | export { ... }
function handleReExport() {
LOOKING_FOR = "export pattern for re-export";
const braceStart = str.indexWithin("{", pos + "export ".length, 5);
const braceEnd = str.indexWithin("}", braceStart + 1, lenDestructure);
LOOKING_FOR = "name of re-exported module";
let moduleStart = str.indexWithin("from ", braceEnd + 1, 10, false);
if (moduleStart === -1) {
// export { ... }
const end = skipNewLines(str, braceEnd);
start = end + 1;
return {
type: "reExportImported",
start: pos,
end,
modules: destructureModules(str.slice(braceStart, braceEnd + 1))
};
}
moduleStart = str.indexWithin(quote, moduleStart, "from ".length + 4);
const moduleEnd = str.indexWithin(quote, moduleStart + 1, lenModuleName);
const end = skipNewLines(str, moduleEnd);
start = end + 1;
return {
type: "reExport",
start: pos,
end,
modules: destructureModules(str.slice(braceStart, braceEnd + 1)),
moduleName: str.slice(moduleStart, moduleEnd + 1)
};
}
function handleExportStar() {
throw new Error("not implemented");
}
function destructureModules(objLiteral) {
return objLiteral
.trim()
.slice(1, -1)
.split(/,\s*/)
.map(i => i.trim())
.filter(i => i)
.map(i => i.split(/\s*\bas\b\s*/));
}
}
// this is same as indexOf, but can stop searching earlier
function indexWithin(needle, from, within = 99, throws = true) {
for (let i = from, L = from + within, j = 0; i < L; ++i) {
if (this.charCodeAt(i) === needle.charCodeAt(j)) {
while (j < needle.length) {
if (this.charCodeAt(i + j) === needle.charCodeAt(j)) {
++j;
} else {
j = 0;
break;
}
}
if (j === needle.length) {
return i;
}
}
}
if (throws) {
throw new Error(
`ParseError: Failed to find \`${needle}\` within ${within} characters from position ${from}` +
(LOOKING_FOR ? ` while looking for ${LOOKING_FOR}` : "") +
"\n\nINPUT STRING:" +
`\n${"*".repeat(20)}\n` +
this +
`\n${"*".repeat(20)}\n`
);
} else {
return -1;
}
}
function skipNewLines(str, i) {
if (str.charAt(i + 1) === ";") ++i;
while (i < str.length && /\s/.test(str.charAt(i))) {
++i;
}
return i;
}