-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
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
Conversation
Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/10893/ |
This doesn't correctly handle enums inside namespaces. You end up with invalid syntax. Input: export namespace A {
export enum B {
A = 1,
B = 2,
C = 3,
}
} Output: "use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.A = void 0;
var A;
exports.A = A;
(function (A) {
export var B;
(function (B) {
B[B["A"] = 1] = "A";
B[B["B"] = 2] = "B";
B[B["C"] = 3] = "C";
})(B || (B = {}));
})(A || (exports.A = A = {})); Config: {
"presets": [
"@babel/env",
"@babel/typescript"
],
"plugins": [
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread"
]
} |
3a2e9fe
to
62f2195
Compare
I addressed the issue that @amaranth brought up. This also meant I added a few more test cases, like multiple namespaces only declaring themselves once, and sharing the name of an enum wont redeclare it. The incremental change and a minor name change. I squashed the commits, rebased to current master, and pushed it to this PR. |
Would love to see this merged! I have a couple projects relying on this feature. I've already built this branch and linked it to my projects to confirm it work for my use cases. |
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.
Hi @Wolvereness, thank you for working on this feature.
We have not been implementing namespaces in Babel for two reasons:
- As you pointed out, there is a big number of caveats. Even if it is not always possible, we try to match the spec closely: that's why, for example, we are testing our parser agains the test262 test suite. Implementing namespaces exactly as TS does is currently impossible because of the cross-file merging but, as you pointed out, even ignoring it leaves an high implementation complexity.
- TypeScript namespaces are pretty much considered derpecated: as TypeScript's PM wrote, there are better alternatives for namespaces. Also, there is a reccomended rule in TSLint to disallow them.
I'll discuss about this PR with the other maintainers during next meeting (it's in about a week, we'll publish the notes right after).
Possible approaches I can think of are:
- Keeps things as-is;
- Implement namespaces as it's done in this PR, even if it has all those known limitations;
- Only accept type-only namespaces, which can be safely removed without affecting any other code.
Anyway, in the meanwhile I suggest publishing the namespaces transformer to npm as a separate plugin. If people run it before the @babel/plugin-typescript
plugin (or preset), namespaces will be transpiled before that it can throw about them not being supported.
This PR also fixes an actual bug, and it would be ideal if you (or anyone else) could open a separate PR which only fixes it, since it is far less controversial than adding namespaces support.
@nicolo-ribaudo namespaces ARE NOT deprecated (or is there any plan to).
Really the only reason NOT to use them at this point is: A: if you are concerned with only writing ts code that conforms to ES6 (but with types) By saying "Were not going to support this thing because it's basically legacy", you are essentially making that decision for the typescript team. This also causes fragmentation in the community and make the build process even more fragile/confusing. If you're not going to support it, because it's too hard or complex to implement, I understand that. But please stop telling people that it's a "legacy feature". |
I believe that the caveats would be uncommon as a limitation for a user. I included them so they would be documented and clearly benign against a common-case (the last of which I'd even put a large gamble a special case could be constructed to trip up the official compiler if types aren't declared fully). As implemented, this should satisfy the overwhelming majority of how namespaces are used. If there is a caveat not listed, then it needs to be brought to my attention. I assumed Babel isn't taking an ideological stance, and more one from how developer time is diverted. With this PR, the time has been put in, and namespaces can be supported by Babel on a best-effort basis, similar to the rest of the typescript implementation.
Type-only namespaces are (should-be) marked with declare, and are already removed when marked as such. https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-typescript/test/fixtures/declarations/erased
I'm not sure this is an actual bug. I asked about it on the slack and was met with radio silence. The behavior of whether or not something is in the scope doesn't seem to matter while the TS plugin runs (it doesn't use scope), and similarly there's a different expectation that an enum would get handled as a class would somewhere else. Or to rephrase, I think the only observable behavior of it relies on this PR. Also, my interest is rather specific to supporting namespaces, so me putting in the effort of another PR is contingent on the outlook for this PR. |
Thanks for linking microsoft/TypeScript#30994. I definetly had a wrong understanding of the current I was playing with this PR on a repl which supports TS and noticed this: // Throws
namespace Bar {
export let a = 2;
a = 3;
}
// Does not throw
namespace Foo {
export function y() {}
y = 3;
} is it expected? |
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.
Sharing the name of the directly enclosing namespace. This requires using a unique identifier as the parameter, which subsequently limits how a project using babel can interact with their namespaces.
I don't fully understand this restriction. Wouldn't it be possible to transpile namespaces like this, to avioid the naming conflict?
namespace A {
export function A() {}
}
let A;
(function (_A) { // _A is generated by path.scope.generateUidIdentifier
function A() {}
_A.A = A;
})(A || (A = {}));
Solving this selectively makes it two-pass. One for figuring out if we should use a unique identifier, and the second for the transforms. We could have it always use an assumed-unique identifier. This creates its own caveat to be at least slightly divergent from an edge (out-of-specification) case in typescript if something reassigns the identifier representing the namespace itself post-initialization. It also creates another caveat; (actual question from me, not hypothetical) does the process for generating a unique identifier consider that it will be for a sub-scope? Those aren't in the |
It should generate an ID which is unique in the whole file. I'm not 100% sure though.
Can you share an example? EDIT: Another solution would be to make our scope tracker treat namespaces as functions. |
I was setting it up and realized it's actually going to hit a type error. I'm going to assume now what I was thinking of isn't really possible, but I'll do a mental proof sometime this weekend to assure myself, before I write the UUID change. |
I actually found a bug testing this PR, not that it's particularly relevant. |
Namespace name clashes are no longer an issue. |
Hi all 👋 We talked about this PR during the last meeting (notes). The outcome was:
|
I'm very glad to hear that!
|
It is basically the same as this options: https://babeljs.io/docs/en/babel-plugin-transform-typescript#options (
Adding URLs sounds good. Our concern was that users who run into this limitations would think it is a bug and report it and a good error message could clarify that these cases are unsupported. Thanks for all the effort. |
path.replaceWith(getDeclaration(t, name)); | ||
path.scope.registerDeclaration(path.parentPath); | ||
} else { | ||
path.parentPath.replaceWith(value); |
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
var foo = {};
export namespace foo { }
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?
Does thir PR fix #10038? |
Added as a fixture (so, not a full-babel transform), and this is the output (I'm not sure if it's correct): let src;
(function (_src) {
let ns1;
(function (_ns) {
class foo {}
_ns.foo = foo;
})(ns1 || (ns1 = _src.ns1 || (_src.ns1 = {})));
let ns2;
(function (_ns2) {
class foo {}
_ns2.foo = foo;
})(ns2 || (ns2 = _src.ns2 || (_src.ns2 = {})));
})(src || (src = {})); |
Yeah, it seems correct. In the issue it didn't even produce an output 😛. Could you commit it?
What do you mean? |
Fixtures are unit-tests. An end-user would rarely need only the plugin that the fixtures test. For example, the typescript-supporting repl has a bit more output. It also changes the use of underscores. "use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// typescript
var src;
(function (src) {
var ns1;
(function (ns1) {
var foo = function foo() {
_classCallCheck(this, foo);
};
ns1.foo = foo;
})(src.ns1 = ns1 || (ns1 = {}));
var ns2;
(function (ns2) {
var foo = function foo() {
_classCallCheck(this, foo);
};
ns2.foo = foo;
})(src.ns2 = ns2 || (ns2 = {}));
})(src || (src = {})); |
Oh I see; the repl output is different because there are the es2015 and stage-2 presets enabled |
Merged at 0a98814 |
Babel's TypeScript implementation has a few unfortunate caveats: https://babeljs.io/docs/en/babel-plugin-transform-typescript#caveats Most notably, the lack of *full* support for namespaces is painful: babel/babel#8244 babel/babel#9785 By precompiling TypeScript code with the actual TypeScript compiler, we can support features like namespaces without relying on Babel. Of course, Babel still handles everything after TypeScript syntax has been removed.
* Use actual TypeScript instead of @babel/preset-typescript. Babel's TypeScript implementation has a few unfortunate caveats: https://babeljs.io/docs/en/babel-plugin-transform-typescript#caveats Most notably, the lack of *full* support for namespaces is painful: babel/babel#8244 babel/babel#9785 By precompiling TypeScript code with the actual TypeScript compiler, we can support features like namespaces without relying on Babel. Of course, Babel still handles everything after TypeScript syntax has been removed. * Test that JSX syntax works in .tsx files.
EDIT(@nicolo-ribaudo) Merged at 0a98814
This adds basic namespace support for TypeScript in babel. Currently, it's an outright error, so this only adds new functionality where a project would otherwise outright fail to run through babel. I recommend adding some type of feature gate for this functionality, until it has been used at-large for some period of time.
The implementation may be best explained by comparing the canonical example that is added as a test case (in out).
Three known limitations (ordered by increasing complexity of respective solution):
Sharing the name of the directly enclosing namespace. This requires using a unique identifier as the parameter, which subsequently limits how a project using babel can interact with their namespaces. Making a dynamic solution, one that uses a unique identifier only when necessary, requires a similar solution to the next item. There is test coverage for this.Exporting mutable members from inside a namespace. This requires a much more comprehensive depth-first visitor that does extensive scope checking, or an independent state machine to track transformed namespace nodes. There is test coverage for this.
Scope sharing. In image form: Link. This would require a greater complexity than the mutable member limitation. In addition, a fully correct version requires having a full type-model spanning every file, effectively impossible using babel's model.