Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement TypeScript namespace support #9785

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
565 changes: 284 additions & 281 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that exported namespaces are not exported if their original declaration isn't.

e.g. In

var foo = {};

export namespace foo { }

foo isn't exported. This PR correctly handles it; do we have a test for this behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not actually valid typescript, but for some reason typescript wont fuss if the namespace itself is empty.

So, I'm a bit confused, what behavior should we be testing?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe TypeScript ignores this because it doesn't emit anything for an empty namespace. That logic likely accidentally makes it ignore the type error as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ok; maybe typescript accepts it because it is considered as a type-only namespace?

}
} 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.
}
Wolvereness marked this conversation as resolved.
Show resolved Hide resolved

// 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 {}
}