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
Closed
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
565 changes: 284 additions & 281 deletions
565
packages/babel-plugin-transform-typescript/src/index.js
Large diffs are not rendered by default.
Oops, something went wrong.
171 changes: 171 additions & 0 deletions
171
packages/babel-plugin-transform-typescript/src/namespace.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
} | ||
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})); | ||
`; | ||
} |
4 changes: 4 additions & 0 deletions
4
...s/babel-plugin-transform-typescript/test/fixtures/declarations/nested-namespace/input.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
; // Otherwise-empty file | ||
export declare namespace P { | ||
export namespace C {} | ||
} |
1 change: 1 addition & 0 deletions
1
.../babel-plugin-transform-typescript/test/fixtures/declarations/nested-namespace/output.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
; // Otherwise-empty file |
35 changes: 35 additions & 0 deletions
35
packages/babel-plugin-transform-typescript/test/fixtures/namespace/canonical/input.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }`); | ||
} | ||
} |
39 changes: 39 additions & 0 deletions
39
packages/babel-plugin-transform-typescript/test/fixtures/namespace/canonical/output.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}`); | ||
} | ||
} |
4 changes: 4 additions & 0 deletions
4
packages/babel-plugin-transform-typescript/test/fixtures/namespace/clobber-class/input.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
class A { } | ||
namespace A { | ||
export const B = 1; | ||
} |
5 changes: 5 additions & 0 deletions
5
packages/babel-plugin-transform-typescript/test/fixtures/namespace/clobber-class/output.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
class A {} | ||
|
||
(function (_A) { | ||
const B = _A.B = 1; | ||
})(A || (A = {})); |
6 changes: 6 additions & 0 deletions
6
packages/babel-plugin-transform-typescript/test/fixtures/namespace/clobber-enum/input.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
enum A { | ||
C = 2, | ||
} | ||
namespace A { | ||
export const B = 1; | ||
} |
9 changes: 9 additions & 0 deletions
9
packages/babel-plugin-transform-typescript/test/fixtures/namespace/clobber-enum/output.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
var A; | ||
|
||
(function (A) { | ||
A[A["C"] = 2] = "C"; | ||
})(A || (A = {})); | ||
|
||
(function (_A) { | ||
const B = _A.B = 1; | ||
})(A || (A = {})); |
3 changes: 3 additions & 0 deletions
3
packages/babel-plugin-transform-typescript/test/fixtures/namespace/clobber-export/input.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export class N {} | ||
export namespace N {} | ||
export default N; |
5 changes: 5 additions & 0 deletions
5
packages/babel-plugin-transform-typescript/test/fixtures/namespace/clobber-export/output.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export class N {} | ||
|
||
(function (_N) {})(N || (N = {})); | ||
|
||
export default N; |
3 changes: 3 additions & 0 deletions
3
packages/babel-plugin-transform-typescript/test/fixtures/namespace/clobber-import/input.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import N from 'n'; | ||
|
||
namespace N {} |
3 changes: 3 additions & 0 deletions
3
packages/babel-plugin-transform-typescript/test/fixtures/namespace/clobber-import/output.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import N from 'n'; | ||
|
||
(function (_N) {})(N || (N = {})); |
36 changes: 36 additions & 0 deletions
36
...ges/babel-plugin-transform-typescript/test/fixtures/namespace/contentious-names/input.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 {} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
foo
isn't exported. This PR correctly handles it; do we have a test for this behavior?There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?