-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Use getters to define live export bindings #33587
Conversation
@@ -20,11 +20,19 @@ x($); | |||
"use strict"; | |||
exports.__esModule = true; | |||
var jquery_1 = require("jquery"); | |||
exports.x = jquery_1.x; | |||
Object.defineProperty(exports, "x", { enumerable: true, get: () => jquery_1.x }); |
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.
In cases like this it actually should be possible to hoist the Object.defineProperty
call itself to happen before any of the other require
calls.
This might make circular imports more compatible.
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.
Digging into this, it looks like you are correct - this should be hoisted. But so should every exported value, whether it's defined as a getter or not. I'm thinking all these cases should be handled as a separate PR.
As it stands, if the property is accessed but the module from which it was re-exporting the value has not been loaded, we won't get the value in either case. The only thing that's affected is enumerability, which is already an issue without this PR. This PR is a step in the right direction and solves #12522. The hoisting is a separate issue IMO.
@@ -20,11 +20,19 @@ x($); | |||
"use strict"; | |||
exports.__esModule = true; |
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.
If you're going with defineProperty
then the __esModule
property also probably should be defined (non-enumerable, non-configurable, non-writable); as it is it's all three.
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 looks like there's some code that already handles using Object.defineProperty
when the script target is not ES3:
TypeScript/src/compiler/transformers/module/module.ts
Lines 1451 to 1478 in 27941d0
function createUnderscoreUnderscoreESModule() { | |
let statement: Statement; | |
if (languageVersion === ScriptTarget.ES3) { | |
statement = createExpressionStatement( | |
createExportExpression( | |
createIdentifier("__esModule"), | |
createLiteral(/*value*/ true) | |
) | |
); | |
} | |
else { | |
statement = createExpressionStatement( | |
createCall( | |
createPropertyAccess(createIdentifier("Object"), "defineProperty"), | |
/*typeArguments*/ undefined, | |
[ | |
createIdentifier("exports"), | |
createLiteral("__esModule"), | |
createObjectLiteral([ | |
createPropertyAssignment("value", createLiteral(/*value*/ true)) | |
]) | |
] | |
) | |
); | |
} | |
setEmitFlags(statement, EmitFlags.CustomPrologue); | |
return statement; | |
} |
var server_4 = require("./server"); | ||
exports.x = server_4.x; | ||
Object.defineProperty(exports, "x", { enumerable: true, get: () => server_4.x }); |
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.
If you do the hoisting it might be possible to write these with a single Object.defineProperties
call, but that needs some state bookkeeping.
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.
As mentioned above, I think the hoisting should be a separate PR. This is something that could be considered if the bookkeeping doesn't complicate things too much. I will note that Babel does not do this, but that doesn't mean TypeScript couldn't.
@Jessidhia @ajafff What's the next step here? |
This needs to be fixed ASAP - the issue where this bug was first reported is already 3 (!) years old, but the TypeScript team doesn't seem to care that much. Predictable re-exports are essential, yet TypeScript doesn't support them. |
@rbuckton can we actively consider this PR? Taking it would reduce the work we'd need to do elsewhere for our own translation to modules. |
@@ -998,8 +998,8 @@ namespace ts { | |||
setOriginalNode( | |||
setTextRange( | |||
createExpressionStatement( | |||
createExportExpression(getExportName(specifier), exportedValue) | |||
), | |||
createExportExpression(getExportName(specifier), exportedValue, /* location */ undefined, /* liveBinding */ true) |
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.
Will unconditionally making this a live binding result in a defineProperty
call for --target ES3
?
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 that we want to support using defineProperty
, and only fall back to property assignment when it’s not supported.
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; | ||
for (var p in m) b(p); | ||
function b(p) { | ||
if (!exports.hasOwnProperty(p)) Object.defineProperty(exports, p, { |
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.
This is not down-level compatible with ES3. You should instead feature-test for Object.create
(since Object.defineProperty
exists in some versions of IE that only supported it for DOM elements, but Object.create
did not exist until full ES5 support landed).
@@ -20,11 +20,19 @@ x($); | |||
"use strict"; | |||
exports.__esModule = true; | |||
var jquery_1 = require("jquery"); | |||
exports.x = jquery_1.x; | |||
Object.defineProperty(exports, "x", { enumerable: true, get: () => jquery_1.x }); |
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 this test and every other test that doesn't have a // @target
defaults to ES3, so many of these test results are actually incorrect as they would not work in the target environment (where Object.defineProperty
is not defined).
@@ -1310,7 +1310,7 @@ namespace ts { | |||
|
|||
case SyntaxKind.NamedImports: | |||
for (const importBinding of namedBindings.elements) { | |||
statements = appendExportsOfDeclaration(statements, importBinding); | |||
statements = appendExportsOfDeclaration(statements, importBinding, /* liveBinding */ true); |
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.
As above, an unconditional live binding will result in code that is invalid in an ES3 target.
@rbuckton I've made the changes to not break the ES3 target with this PR:
What would be your thoughts on moving the declaration of |
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; | ||
for (var p in m) b(p); | ||
function b(p) { | ||
if (!exports.hasOwnProperty(p)) Object.defineProperty(exports, p, { |
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.
This test doesn't look like it was updated after the changes to the helper.
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; | ||
for (var p in m) b(p); | ||
function b(p) { | ||
if (!exports.hasOwnProperty(p)) Object.defineProperty(exports, p, { |
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.
This test result is also out of date
createLiteral(name), | ||
createObjectLiteral([ | ||
createPropertyAssignment("enumerable", createLiteral(/*value*/ true)), | ||
createPropertyAssignment("get", createArrowFunction( |
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.
You need to use createFunctionExpression
instead of createArrowFunction
. The module
transform is run after all other language-specific transforms have been run, so the arrow function won't be correctly down-leveled when targeting ES5.
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; | ||
for (var p in m) b(p); | ||
function b(p) { | ||
if (!exports.hasOwnProperty(p)) Object.defineProperty(exports, p, { |
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.
This test result is also out of date
The runtime check is still somewhat useful if you are targeting ES3 but end up running in an ES5+ environment. We also need to make this change to the |
Perhaps we should rename the // NOTE: below does feature-test once for whole file on the first call to `__export`
function __export(m, p) {
__export = Object.create
? function (m, p) { Object.defineProperty(exports, p, { enumerable: false, get: function() { return m[p]; } }); }
: function (m, p) { exports[p] = m[p]; };
__export(m, p);
} Then we could cut down on the repetition of the function __exportStar(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) __export(m, p);
} @weswigham, @DanielRosenwasser any thoughts on this? We'd also have to do the same for tslib. |
Right now |
|
@mlrawlings you probably also need to merge in |
@rbuckton eh. It might be OK to do that. I'd still rather do that refactoring separately from the emit feature of proper reexported live bindings, just to reduce the upfront review/design work on @mlrawlings here. |
We spoke about this at our last design meeting and said we wanted it for 3.8. @mlrawlings can you update this branch to fix up the merge conflicts? |
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.
This PR also needs to update __importStar
as it currently copies the values of each property of the imported namespace onto another object. It should use getters as well.
function b(p) { | ||
if (!exports.hasOwnProperty(p)) | ||
Object.create | ||
? Object.defineProperty(exports, p, { |
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.
Not a fan of how this gets checked on every iteration of the loop. In other places, we just decide up front what the helper is going to be and use that.
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.
Looks like there are a few outstanding changes requested.
To help me track our PR backlog, I'm going to request changes officially.
Fixes #12522
Fixes #6366
When re-exporting a binding, it should remain live instead of being a snapshot. This PR uses the same approach as Babel: use getters instead of property assignments .
Summary of Changes
Add option for getters in createExportExpression
The
createExportExpression
has a newliveBinding
argument that, if true, causes a getter to be defined rather than using a property assignment. In the cases where liveBinding is needed, this argument is passed as true. Specifically, 2 cases are handled:export from
liveBinding
is passed from visitExportDeclarationre-exporting an import
liveBinding
is passed from appendExportsOfImportDeclaration → appendExportsOfDeclaration → appendExportStatement → createExportStatementUse getters in exportStarHelper
The
exportStarHelper
has also been updated to define getters rather than use a property assignment to handle the following case:Tests
No new tests have been added as the existing suite adequately covers these cases, but the baseline for 50 of these tests have been updated to reflect the desired output.