packages/babel-plugin-transform-typescript/src/namespace.js
import { template } from "@babel/core";
export default function transpileNamespace(path, t, allowNamespaces) {
if (path.node.declare || path.node.id.type === "StringLiteral") {
path.remove();
return;
}
if (!allowNamespaces) {
throw path.hub.file.buildCodeFrameError(
path.node.id,
"Namespace not marked type-only declare." +
" Non-declarative namespaces are only supported experimentally in Babel." +
" To enable and review caveats see:" +
" https://babeljs.io/docs/en/babel-plugin-transform-typescript",
);
}
const name = path.node.id.name;
const value = handleNested(path, t, t.cloneDeep(path.node));
const bound = path.scope.hasOwnBinding(name);
if (path.parent.type === "ExportNamedDeclaration") {
if (!bound) {
path.parentPath.insertAfter(value);
path.replaceWith(getDeclaration(t, name));
path.scope.registerDeclaration(path.parentPath);
} else {
path.parentPath.replaceWith(value);
}
} else if (bound) {
path.replaceWith(value);
} else {
path.scope.registerDeclaration(
path.replaceWithMultiple([getDeclaration(t, name), value])[0],
);
}
}
function getDeclaration(t, name) {
return t.variableDeclaration("let", [
t.variableDeclarator(t.identifier(name)),
]);
}
function getMemberExpression(t, name, itemName) {
return t.memberExpression(t.identifier(name), t.identifier(itemName));
}
function handleNested(path, t, node, parentExport) {
const names = new Set();
const realName = node.id;
const name = path.scope.generateUid(realName.name);
const namespaceTopLevel = node.body.body;
for (let i = 0; i < namespaceTopLevel.length; i++) {
const subNode = namespaceTopLevel[i];
// The first switch is mainly to detect name usage. Only export
// declarations require further transformation.
switch (subNode.type) {
case "TSModuleDeclaration": {
const transformed = handleNested(path, t, subNode);
const moduleName = subNode.id.name;
if (names.has(moduleName)) {
namespaceTopLevel[i] = transformed;
} else {
names.add(moduleName);
namespaceTopLevel.splice(
i++,
1,
getDeclaration(t, moduleName),
transformed,
);
}
continue;
}
case "TSEnumDeclaration":
case "FunctionDeclaration":
case "ClassDeclaration":
names.add(subNode.id.name);
continue;
case "VariableDeclaration":
for (const variable of subNode.declarations) {
names.add(variable.id.name);
}
continue;
default:
// Neither named declaration nor export, continue to next item.
continue;
case "ExportNamedDeclaration":
// Export declarations get parsed using the next switch.
}
// Transform the export declarations that occur inside of a namespace.
switch (subNode.declaration.type) {
case "TSEnumDeclaration":
case "FunctionDeclaration":
case "ClassDeclaration": {
const itemName = subNode.declaration.id.name;
names.add(itemName);
namespaceTopLevel.splice(
i++,
1,
subNode.declaration,
t.expressionStatement(
t.assignmentExpression(
"=",
getMemberExpression(t, name, itemName),
t.identifier(itemName),
),
),
);
break;
}
case "VariableDeclaration":
if (subNode.declaration.kind !== "const") {
throw path.hub.file.buildCodeFrameError(
subNode.declaration,
"Namespaces exporting non-const are not supported by Babel." +
" Change to const or see:" +
" https://babeljs.io/docs/en/babel-plugin-transform-typescript",
);
}
for (const variable of subNode.declaration.declarations) {
variable.init = t.assignmentExpression(
"=",
getMemberExpression(t, name, variable.id.name),
variable.init,
);
}
namespaceTopLevel[i] = subNode.declaration;
break;
case "TSModuleDeclaration": {
const transformed = handleNested(
path,
t,
subNode.declaration,
t.identifier(name),
);
const moduleName = subNode.declaration.id.name;
if (names.has(moduleName)) {
namespaceTopLevel[i] = transformed;
} else {
names.add(moduleName);
namespaceTopLevel.splice(
i++,
1,
getDeclaration(t, moduleName),
transformed,
);
}
}
}
}
// {}
let fallthroughValue = t.objectExpression([]);
if (parentExport) {
fallthroughValue = template.expression.ast`
${parentExport}.${realName} || (
${parentExport}.${realName} = ${fallthroughValue}
)
`;
}
return template.statement.ast`
(function (${t.identifier(name)}) {
${namespaceTopLevel}
})(${realName} || (${realName} = ${fallthroughValue}));
`;
}