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

Use getters to define live export bindings #33587

Closed
wants to merge 3 commits into from

Conversation

mlrawlings
Copy link
Contributor

@mlrawlings mlrawlings commented Sep 25, 2019

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 new liveBinding 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

export { x } from “foo”;

liveBinding is passed from visitExportDeclaration

re-exporting an import

import { x } from “foo”;
export { x };

liveBinding is passed from appendExportsOfImportDeclarationappendExportsOfDeclarationappendExportStatementcreateExportStatement

Use getters in exportStarHelper

The exportStarHelper has also been updated to define getters rather than use a property assignment to handle the following case:

export * from “foo”;

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.

@msftclas
Copy link

msftclas commented Sep 25, 2019

CLA assistant check
All CLA requirements met.

@@ -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 });

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.

Copy link
Contributor Author

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;

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.

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 looks like there's some code that already handles using Object.defineProperty when the script target is not ES3:

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 });

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.

Copy link
Contributor Author

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.

@mlrawlings
Copy link
Contributor Author

@Jessidhia @ajafff What's the next step here?

@squidfunk
Copy link

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.

@weswigham
Copy link
Member

@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)
Copy link
Member

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?

Copy link
Contributor

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, {
Copy link
Member

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 });
Copy link
Member

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);
Copy link
Member

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.

@mlrawlings
Copy link
Contributor Author

mlrawlings commented Dec 16, 2019

@rbuckton I've made the changes to not break the ES3 target with this PR:

  1. createExportExpression now checks language version and only respects liveBinding if the target is not ES3.
  2. exportStarHelper checks for Object.create as suggested and uses plain property assignment if the property is falsy.

What would be your thoughts on moving the declaration of exportStarHelper into the transformModule function? It would then have access to languageVersion and would remove the need for a runtime check.

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, {
Copy link
Member

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, {
Copy link
Member

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(
Copy link
Member

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, {
Copy link
Member

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

@rbuckton
Copy link
Member

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 __exportStar helper in tslib for use with the --importHelpers flag, and we don't want to have separate ES3 and ES5+ versions of that external helper.

@rbuckton
Copy link
Member

rbuckton commented Dec 16, 2019

Perhaps we should rename the __export helper to __exportStar and implement a new __export helper for single exports that has the same feature-test for down leveling:

// 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 Object.defineProperty calls a bit, and reimplement __exportStar in terms of __export:

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.

@weswigham
Copy link
Member

any thoughts on this?

Right now __export isn't a real helper, right? Since it closes over exports and always has to be redefined in scope. At least for now, I think not breaking that is probably correct? Minimally, I'd do that break in a separate PR.

@rbuckton
Copy link
Member

__export is a "scoped" helper, meaning that it closes over something in the lexical scope, although when we use --importHelpers we use an "unscoped" helper named __exportStar that doesn't close over anything in the lexical scope.

@weswigham
Copy link
Member

@mlrawlings you probably also need to merge in master to get CI - I'm pretty sure CI refuses to run while there's a merge conflict.

@weswigham
Copy link
Member

@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.

@DanielRosenwasser DanielRosenwasser added Breaking Change Would introduce errors in existing code Update Docs on Next Release Indicates that this PR affects docs labels Dec 20, 2019
@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Dec 23, 2019

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?

Copy link
Contributor

@ajafff ajafff left a 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, {
Copy link
Member

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.

Copy link
Member

@sandersn sandersn left a 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.

@weswigham
Copy link
Member

@sandersn this is superseded by #35967, which is an update of this with feedback taken into account.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Breaking Change Would introduce errors in existing code Update Docs on Next Release Indicates that this PR affects docs
Projects
None yet
10 participants