Skip to content

[Fix 13709] - Emit __esmodule #13871

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

Merged
merged 21 commits into from
Feb 13, 2017
Merged

[Fix 13709] - Emit __esmodule #13871

merged 21 commits into from
Feb 13, 2017

Conversation

yuit
Copy link
Contributor

@yuit yuit commented Feb 3, 2017

Fix #13709. It is a big change because of the baselines. The meat of the change is in "module/modules.ts"

Couple question:

  • What should happen when user already defined "__esmodule"? Currently, we will not emit our version of "__esmodule". Babel gives an error for using such variable name. We will issue an error

  • Should we emit "__esmodule" for export import x = require ...? Yes

@@ -378,6 +383,10 @@ namespace ts {
// Append the 'export =' statement if provided.
addExportEqualsIfNeeded(statements, /*emitAsReturn*/ true);

if (shouldAppendUnderscoreUnderscoreEsModule) {
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we set it if we have export = ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We didn't set shouldAppendUnderscoreUnderscoreEsModule here. This is after we have passed through transform module already and we just check if anyone set the flag

@@ -657,6 +666,8 @@ namespace ts {
}

const generatedName = getGeneratedNameForNode(node);
shouldAppendUnderscoreUnderscoreEsModule = true;
Copy link
Member

Choose a reason for hiding this comment

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

shouldAppendEsModuleMarker


const parseTreeNode = getParseTreeNode(node);
if (!shouldAppendUnderscoreUnderscoreEsModule) {
// class declaration get down-level transformed to be variable statement
Copy link
Member

Choose a reason for hiding this comment

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

declaration -> declarations
statement -> statements

Copy link
Contributor

@rbuckton rbuckton left a comment

Choose a reason for hiding this comment

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

I think we should make any top-level exported declaration of __esModule an error.

@@ -820,6 +831,14 @@ namespace ts {
let statements: Statement[];
let variables: VariableDeclaration[];
let expressions: Expression[];

const parseTreeNode = getParseTreeNode(node);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need this check? Wouldn't setting shouldAppendEsModuleMarker to true in createExportExpression be sufficient? Basically, if I export anything other than via an export=, then I need to append __esModule. Would that be correct? Alternatively, always add it if you don't have an export= (e.g. currentModuleInfo.exportEquals === false).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So just setting on in createExportExpression won't be enough because only VariableStatement with bindingPattern will call into the function.

Also we can't just check for currentModuleInfo.exportEquals === false
because we will be issue extra __esModule even though it doesn't need one

For example:

    export enum SignatureFlags {
        None = 0,
        IsIndexer = 1,
        IsStringIndexer = 1 << 1,
        IsNumberIndexer = 1 << 2,
    }

will get __esModule as well

Copy link
Contributor

Choose a reason for hiding this comment

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

Why wouldn't we emit __esModule here? Perhaps I'm not understanding when we do/don't want to emit this marker.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rbuckton After some thought, I think we should emit one here. Though in the following case, I don't think we should. By checking !currentModuleIndo.exportEqual, we will emit the marker still.

import * as Foo from "Foo"

@@ -21921,6 +21925,22 @@ namespace ts {
return checkLetConstNames && checkGrammarNameInLetOrConstDeclarations(node.name);
}

function checkESModuleMarker(name: Identifier | BindingPattern): boolean {
if (name.kind === SyntaxKind.Identifier) {
if (unescapeIdentifier(name.text) === "__esModule") {
Copy link
Contributor

Choose a reason for hiding this comment

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

The error should not be shown in ambient contexts and if --noEmit is set.

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 we discuss, module kind of system should not show an error as well

@@ -671,6 +671,10 @@
"category": "Error",
"code": 1215
},
"Identifier expected. '__esModule' is reserved as a marker for transpiled ES2015 module.": {
Copy link
Member

Choose a reason for hiding this comment

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

I think "Identifier expected" is a confusing bit of information that probably shouldn't be reported here.

Copy link
Member

@DanielRosenwasser DanielRosenwasser Feb 7, 2017

Choose a reason for hiding this comment

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

I think this would be a better message:

'__esModule' is reserved as a top-level export when transforming ECMAScript modules.

Copy link
Contributor Author

@yuit yuit Feb 7, 2017

Choose a reason for hiding this comment

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

I like the new message!. But Identifier expected should be there I think ( similar to how we report similar error for reserved word)

Copy link
Member

Choose a reason for hiding this comment

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

I don't think the Identifier expected is useful in any way. __esModule is an identifier. 😄

@yuit
Copy link
Contributor Author

yuit commented Feb 7, 2017

@@ -88,11 +88,14 @@ namespace ts {
addRange(statements, endLexicalEnvironment());
addExportEqualsIfNeeded(statements, /*emitAsReturn*/ false);

if (!currentModuleInfo.exportEquals) {
append(statements, createUnderscoreUnderscoreESModule());
Copy link

@Jessidhia Jessidhia Feb 8, 2017

Choose a reason for hiding this comment

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

Not really familiar with TS codebase to know if this is correct and/or easily fixable, but the __esModule marker should be created as soon as possible. It needs to be visible from circular imports.

Copy link
Contributor Author

@yuit yuit Feb 9, 2017

Choose a reason for hiding this comment

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

@Kovensky do you mean __esModule has to be emitted as a first statement in the output to prevent circular imports? Could you elaborate on your comments? thanks!

Copy link
Contributor

@rbuckton rbuckton Feb 10, 2017

Choose a reason for hiding this comment

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

@yuit not to prevent circular imports, but rather to enable them. Consider:

// a.ts
import b from './b';
export default function(x) {
    return x ? b(false) : 1;
}

// b.ts
import a from './a';
export default function(x) {
    return x ? a(false) : 2;
}

Both a.ts and b.ts should be recognized as an ES module as early as possible, before the first require() is called in either module.

Consider the following scenario, where we emit __esModule at the end:

// a.js
var b_1 = require("./b");
exports.default = function (x) {
    return x ? b_1(false) : 1;
};
Object.defineProperty(exports, "__esModule", { value: true });

// b.js
var a_1 = require("./a");
exports.default = function (x) {
    return x ? a_1(false) : 2;
}
Object.defineProperty(exports, "__esModule", { value: true });

Assuming a.js is loaded first:

  1. A module object will be created for a.js and stored in Node's module cache. At this point, Node (or rather, some future version) does not know whether it is an ES module or a CJS module.
  2. The a.js module body begins to execute and a.js invokes a require() for b.js.
  3. A module object will be created for b.js and stored in Node's module cache.
  4. The b.js module body begins to execute the module body and invokes a require() for a.js.
  5. Node cannot see the __esModule marker on the exports object for a.js and doesn't know is an ES module, and therefore cannot provide the right answer to b.js.

Now consider the same scenario, with the __esModule marker at the beginning:

// a.js
Object.defineProperty(exports, "__esModule", { value: true });
var b_1 = require("./b");
exports.default = function (x) {
    return x ? b_1(false) : 1;
};

// b.js
Object.defineProperty(exports, "__esModule", { value: true });
var a_1 = require("./a");
exports.default = function (x) {
    return x ? a_1(false) : 2;
}

Assuming a.js is loaded first:

  1. A module object will be created for a.js and stored in Node's module cache. At this point, Node (or rather, some future version) does not know whether it is an ES module or a CJS module.
  2. The a.js module body begins to execute and a.js invokes a require() for b.js.
  3. A module object will be created for b.js and stored in Node's module cache.
  4. The b.js module body begins to execute the module body and invokes a require() for a.js.
  5. Node can see the __esModule marker on the exports object for a.js and knows it can treat it as an ES module.

However, export = cannot be used circularly to begin with so this whole topic is possibly moot.

Copy link
Contributor

Choose a reason for hiding this comment

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

@DanielRosenwasser, is this an accurate description of the mechanics?

Copy link
Contributor Author

@yuit yuit Feb 10, 2017

Choose a reason for hiding this comment

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

@rbuckton thank you for the thorough explanation! To be safe, we should then emit __esModule first. this seems to be safer than emit at the end. However, it will be after some helpers (as those are added by previous transformer)

@yuit
Copy link
Contributor Author

yuit commented Feb 13, 2017

talk offline with @rbuckton, he gives a green light

@yuit yuit merged commit bc1058e into master Feb 13, 2017
@yuit yuit deleted the master-fix13709 branch February 13, 2017 20:32
@popomore
Copy link

I'm requiring a ts file that has compiled from a commonjs file, how can I check is it export defualt?

Is it simply checking whether contain default property?

popomore added a commit to eggjs/core that referenced this pull request Feb 23, 2017
Compile such as babel, typescript will compile es module always
containing `__esModule`, we should do right things.

Typescript 2.2 fix this bug in
microsoft/TypeScript#13871
popomore added a commit to eggjs/core that referenced this pull request Feb 23, 2017
Compile such as babel, typescript will compile es module always
containing `__esModule`, we should do right things.

Typescript 2.2 fix this bug in
microsoft/TypeScript#13871
popomore added a commit to eggjs/core that referenced this pull request Feb 23, 2017
Compile such as babel, typescript will compile es module always
containing `__esModule`, we should do right things.

Typescript 2.2 fix this bug in
microsoft/TypeScript#13871
fengmk2 pushed a commit to eggjs/core that referenced this pull request Feb 23, 2017
Compile such as babel, typescript will compile es module always
containing `__esModule`, we should do right things.

Typescript 2.2 fix this bug in
microsoft/TypeScript#13871
@mhegazy
Copy link
Contributor

mhegazy commented Feb 23, 2017

I'm requiring a ts file that has compiled from a commonjs file, how can I check is it export defualt?
Is it simply checking whether contain default property?

yes. TypeScript does not change any thing about your default export. if the module has a property called "default" then it has a default export.

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants