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

Add refactoring to convert CommonJS module to ES6 module #19916

Merged
7 commits merged into from
Jan 9, 2018
Merged

Conversation

ghost
Copy link

@ghost ghost commented Nov 10, 2017

Fixes #19719

@ghost ghost requested a review from amcasey November 10, 2017 17:44
@ghost ghost force-pushed the convertToEsModule branch 2 times, most recently from 4118c0c to 517715a Compare November 10, 2017 18:58
@amcasey
Copy link
Member

amcasey commented Nov 13, 2017

Someone who understands modules should probably take a look too.

@ghost
Copy link
Author

ghost commented Nov 13, 2017

@uniqueiniquity @minestarks @armanio123 @aozgaa Someone want to take this on?

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

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

Notes on all files but convertToEs6Module.ts.

@@ -0,0 +1,20 @@
/// <reference path='fourslash.ts' />

// Test that we leave it alone if the name is a keyword.
Copy link
Member

Choose a reason for hiding this comment

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

Copy-paste error?


const y = 1;
const _y = y;
export { _y as y };
Copy link
Member

Choose a reason for hiding this comment

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

Presumably, there's no conflict between this y and the constant above?

Copy link
Author

Choose a reason for hiding this comment

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

Right, the y in as y is the exported name but isn't a local variable.

newContent: `import x from "x";
import _x from "x";
const [a, b] = _x;
import __x from "x";
Copy link
Member

Choose a reason for hiding this comment

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

Are imports supposed to go at the top?

Copy link
Author

Choose a reason for hiding this comment

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

Some people lint for that style but it's not a language rule. In this case I think it makes sense to put the only use of _x next to it. It's going to be ugly anyway so hopefully people aren't doing unusual destructuring of a require().

import { d } from "c";
const [a, b] = d;
import { a as _a } from "c";
const [a, b] = _a; // Test that we avoid shadowing 'a'`,
Copy link
Member

Choose a reason for hiding this comment

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

This comment could be clearer about which a is (not) being shadowed.

@@ -394,6 +394,16 @@ namespace ts {
return result;
}

export function mapIter<T, U>(iter: Iterator<T>, mapFn: (x: T) => U): U[] {
Copy link
Member

Choose a reason for hiding this comment

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

Naively, I would have expected this to return an Iterator.

@@ -515,12 +525,23 @@ namespace ts {
return result || array;
}

export function mapAllOrFail<T, U>(array: ReadonlyArray<T>, mapFn: (x: T, i: number) => U | undefined): U[] | undefined {
Copy link
Member

Choose a reason for hiding this comment

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

You can pass a function that only takes x, right?

Copy link
Author

Choose a reason for hiding this comment

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

👍

const result: U[] = [];
for (let i = 0; i < array.length; i++) {
const mapped = mapFn(array[i], i);
if (!mapped) {
Copy link
Member

Choose a reason for hiding this comment

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

Should this be an explicit check for undefined? Or is a falsy value considered to indicate failure?

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

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

Seems reasonable, but I don't understand it deeply.

* Contains an entry for each renamed export.
* This is necessary because `exports.x = 0;` does not declare a local variable.
* Converting this to `export const x = 0;` would declare a local, so we must be careful to avoid shadowing.
* If there would be shadowing at either the declaration or at any reference to `exports.x` (now just `x`), we must conver to:
Copy link
Member

Choose a reason for hiding this comment

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

Typo "conver to"

});
}

function forEachExportReference(sourceFile: SourceFile, cb: (node: PropertyAccessExpression, isAssignment: boolean) => void): void {
Copy link
Member

Choose a reason for hiding this comment

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

isAssignment means that node is the LHS of an assignment?

}

/** Whether `module.exports =` was changed to `export default` */
type ModuleExportsChanged = boolean;
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity, why alias boolean like this?

Copy link
Author

Choose a reason for hiding this comment

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

So I don't have to keep documenting what the return value means -- convertAssignment(...): boolean doesn't really indicate that well.

});
if (foundImport) {
// useNonAdjustedEndPosition to ensure we don't eat the newline after the statement.
changes.replaceNodeWithNodes(sourceFile, statement, newNodes, { nodeSeparator: "\n", useNonAdjustedEndPosition: true });
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 the refactoring context might contain a newline character.

switch (prop.kind) {
case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
// TODO: Maybe we should handle this? See fourslash test `refactorConvertToEs6Module_export_object_shorthand.ts`.
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure I understand what work remains to be done here.

Copy link
Author

@ghost ghost Nov 14, 2017

Choose a reason for hiding this comment

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

We could transpile that test to just export function f() {} instead of function f() {} export default { f };. But it would be work, since we would have to go backwards from the module.exports = statement to get a set of nodes that need to be changed, and I don't know how common it is to write a module like that.

case SyntaxKind.CallExpression: {
const call = parent as CallExpression;
if (isRequireCall(call, /*checkArgumentIsStringLiteral*/ false)) {
changes.replaceNode(importingFile, parent, createPropertyAccess(getSynthesizedDeepClone(call), "default"));
Copy link
Member

Choose a reason for hiding this comment

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

I would have guessed there was a constant for "default" somewhere.

Copy link
Author

Choose a reason for hiding this comment

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

Doesn't look like it, "default" occurs 67 times in our code. You could make a separate PR for that.

case SyntaxKind.ClassExpression:
// `exports.C = class {}` --> `export class C {}`
return classExpressionToDeclaration(name, modifiers, exported as ClassExpression);
default:
Copy link
Member

Choose a reason for hiding this comment

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

Does this cover a reasonably short list of kinds? It would be nice if we could have the compiler notify us when a new case requires coverage.

Copy link
Author

Choose a reason for hiding this comment

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

Unfortunately Expression is a huge list of kinds. But only function/class expressions can be converted to declarations, all others should be left as expressions.

const { text, originalKeywordKind } = node.name;
if (!res.has(text) && (originalKeywordKind !== undefined && isNonContextualKeyword(originalKeywordKind)
|| checker.resolveName(node.name.text, node, SymbolFlags.Value, /*includeGlobals*/ false))) {
res.set(text, makeUniqueName(`_${text}`, identifiers));
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it redundant to prepend an underscore?

Copy link
Author

Choose a reason for hiding this comment

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

text is a keyword name; it doesn't appear in the identifiers list, but still needs the underscore to avoid being a keyword. And then if there happens to be an identifier _class we need to change to __class.

@@ -920,6 +921,7 @@ namespace ts {
nameNotFoundMessage: DiagnosticMessage,
nameArg: __String | Identifier,
isUse: boolean,
includeGlobals: boolean,
Copy link
Contributor

Choose a reason for hiding this comment

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

seems wierd to have to opt-in resolving a global. i would expect this be the opposite, that you opt out-in to skip globals

export default 0;

export * from "./b";
export { default } from "./b";`,
Copy link
Contributor

@mhegazy mhegazy Nov 30, 2017

Choose a reason for hiding this comment

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

this seems wrong to me.. or at least having both exports seem wrong.. i think you either want one or the other.. and to a big extend it depends on what the shape of "./b" is. if "./b" does not have a default export, this statement is an error under ES6 semantics.

I would say we should do one or the other, i.e. always do export * from "./b" unless ",/b" has only one export that happens to be default (or export = for that matter).

actionDescription: "Convert to ES6 module",
newContent: `import x from "x";
const y = 0;
import { z } from "z";`,
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure i understand why this is { z } an not import _z from "z"; const {z} = _z;

Copy link
Author

Choose a reason for hiding this comment

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

If we had this:

const zNamespace = require("z");
zNamespace.z();

We would translate to this:

import { z } from "z";
z();

right?
Using a destructure in commonjs (const { z } = require("z");) behaves equivalently to the first example so I think we should upgrade it to es6 too.

Copy link
Contributor

Choose a reason for hiding this comment

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

well it depends on what "z" is. if it is ES6 module, then it is fine. if it a cjs file, then it shoudl be import zNamspace from "z"; zNamespace.z().

2 questions:

  1. what if "z" is another file in the project, and the user is going to run the refactoring on "z" next, should we assume it is a default? or that it is a namespace?
  2. What do we do in absence of information, e.g. if this is a module from node_modules, or one we have not seen before.. i would say it safe to assume that everything is cjs unless proven otherwise..

I think the important part here is for us to be consistent between import and export. either everything is a default, or everything is a namespace. let's grab @DanielRosenwasser tomorrow and talk it out.

Copy link
Author

Choose a reason for hiding this comment

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

Talked with @DanielRosenwasser and I think it's fine to use a named import regardless of the thing we're importing from. @DanielRosenwasser mentioned that we could also choose to provide a second codefix that would always use default imports. But I think we should wait and see if anyone actually wants that, because I think most people will want named imports.

const [] = aB;
import A from "0a";
const [] = A;
import _A from "1a";
Copy link
Contributor

Choose a reason for hiding this comment

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

ideally we would have all the import statements at the top and not interleaved with the statements

Copy link
Author

Choose a reason for hiding this comment

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

Similar comment in #19916 (comment)
I don't think that would be easy to do as we are replacing all of the ... = require(...) statements in place -- we're not guaranteed that these will all be in a row at the top of the file anyway.
This case should be pretty rare though since it's unusual to destructure a module into an array.

refactorName: "Convert to ES6 module",
actionName: "Convert to ES6 module",
actionDescription: "Convert to ES6 module",
newContent: 'import { x, y as z } from "x";',
Copy link
Contributor

Choose a reason for hiding this comment

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

same as my earlier comment, i thought this would be a default import.

refactorName: "Convert to ES6 module",
actionName: "Convert to ES6 module",
actionDescription: "Convert to ES6 module",
newContent: `import { y } from "x";
Copy link
Contributor

Choose a reason for hiding this comment

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

and this.

@@ -101,7 +101,7 @@ namespace ts {
return node;
}

function createLiteralFromNode(sourceNode: StringLiteral | NumericLiteral | Identifier): StringLiteral {
function createLiteralFromNode(sourceNode: StringLiteralLike | NumericLiteral | Identifier): StringLiteral {
Copy link
Member

Choose a reason for hiding this comment

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

This isn't internal, but it uses StringLiteralLike. You can technically make an internal overload for it, or use NoSubstitutionTemplateLiteral

Copy link
Author

@ghost ghost Dec 4, 2017

Choose a reason for hiding this comment

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

This isn't exported so it won't show up in the public API. API baseline tests would have told us if we tried to use an internal type in a public api.

@ghost
Copy link
Author

ghost commented Dec 6, 2017

@DanielRosenwasser @mhegazy Good to go?

@mhegazy
Copy link
Contributor

mhegazy commented Jan 9, 2018

@andy-ms can you please refresh and merge.

@ghost ghost merged commit 8bce69e into master Jan 9, 2018
@ghost ghost deleted the convertToEsModule branch January 9, 2018 21:15
@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
This pull request was closed.
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.

3 participants