Skip to content

Commit

Permalink
Implement TypeScript namespace support (#9785)
Browse files Browse the repository at this point in the history
* Add module tests for typescript namespace transform

Fixes #8244, fixes #10038
  • Loading branch information
Wolvereness authored and nicolo-ribaudo committed Jun 30, 2019
1 parent 8bf9714 commit 0a98814
Show file tree
Hide file tree
Showing 39 changed files with 959 additions and 289 deletions.
571 changes: 287 additions & 284 deletions packages/babel-plugin-transform-typescript/src/index.js

Large diffs are not rendered by default.

171 changes: 171 additions & 0 deletions packages/babel-plugin-transform-typescript/src/namespace.js
@@ -0,0 +1,171 @@
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}));
`;
}
@@ -0,0 +1,4 @@
; // Otherwise-empty file
export declare namespace P {
export namespace C {}
}
@@ -0,0 +1 @@
; // Otherwise-empty file
@@ -0,0 +1,35 @@
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}

const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;

export class LettersOnlyValidator implements StringValidator {
constructor() {
console.log("1");
}
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}

export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}

let strings = ["Hello", "98052", "101"];

let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

for (let s of strings) {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
}
@@ -0,0 +1,39 @@
let Validation;

(function (_Validation) {
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;

class LettersOnlyValidator {
constructor() {
console.log("1");
}

isAcceptable(s) {
return lettersRegexp.test(s);
}

}

_Validation.LettersOnlyValidator = LettersOnlyValidator;

class ZipCodeValidator {
isAcceptable(s) {
return s.length === 5 && numberRegexp.test(s);
}

}

_Validation.ZipCodeValidator = ZipCodeValidator;
})(Validation || (Validation = {}));

let strings = ["Hello", "98052", "101"];
let validators = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

for (let s of strings) {
for (let name in validators) {
console.log(`"${s}" - ${validators[name].isAcceptable(s) ? "matches" : "does not match"} ${name}`);
}
}
@@ -0,0 +1,4 @@
class A { }
namespace A {
export const B = 1;
}
@@ -0,0 +1,5 @@
class A {}

(function (_A) {
const B = _A.B = 1;
})(A || (A = {}));
@@ -0,0 +1,6 @@
enum A {
C = 2,
}
namespace A {
export const B = 1;
}
@@ -0,0 +1,9 @@
var A;

(function (A) {
A[A["C"] = 2] = "C";
})(A || (A = {}));

(function (_A) {
const B = _A.B = 1;
})(A || (A = {}));
@@ -0,0 +1,3 @@
export class N {}
export namespace N {}
export default N;
@@ -0,0 +1,5 @@
export class N {}

(function (_N) {})(N || (N = {}));

export default N;
@@ -0,0 +1,3 @@
import N from 'n';

namespace N {}
@@ -0,0 +1,3 @@
import N from 'n';

(function (_N) {})(N || (N = {}));
@@ -0,0 +1,36 @@
namespace N {
namespace N {}
namespace constructor {}
namespace length {}
namespace concat {}
namespace copyWithin {}
namespace fill {}
namespace find {}
namespace findIndex {}
namespace lastIndexOf {}
namespace pop {}
namespace push {}
namespace reverse {}
namespace shift {}
namespace unshift {}
namespace slice {}
namespace sort {}
namespace splice {}
namespace includes {}
namespace indexOf {}
namespace join {}
namespace keys {}
namespace entries {}
namespace values {}
namespace forEach {}
namespace filter {}
namespace map {}
namespace every {}
namespace some {}
namespace reduce {}
namespace reduceRight {}
namespace toLocaleString {}
namespace toString {}
namespace flat {}
namespace flatMap {}
}

0 comments on commit 0a98814

Please sign in to comment.