diff --git a/NOTES.md b/NOTES.md index 0a199ff..b53d3b0 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1,10 +1,15 @@ # Notes -## React.Node -> React.ReactElement +## Strip React.Node from function component return types One issue we encountered was most codemods convert `React.Node` to `React.ReactNode` when they're a function return type. While the names are similar, they behave [differently](https://stackoverflow.com/questions/58123398/when-to-use-jsx-element-vs-reactnode-vs-reactelement?rq=1). In TypeScript, `React.ReactNode` is primarily used to type things that can be children of a React node, and it includes things like booleans or null. When we return `React.Node`, we're usually annotating a functional component that can be instantiated in JSX (``). In TypeScript that's inferred [as React.ReactElement or JSX.Element](https://github.com/TypeScript-cheatsheets/react#function-components). This also needs to be done for the `render` method of class components as well, despite some of the [React types](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L3087) suggesting otherwise. If you leave it as `React.ReactNode` you'll receive the error `'Component' cannot be used as a JSX component`. In addition, if the function returns `null`, we have to add a `| null` to allow it. Strings and numbers will throw an error if returned in TS. [TS Playground](https://www.TypeScriptlang.org/play?#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wG4Aoc4AOxiSk3STgAUcwBnOAb3Ln7gcA-AC5BMKDQDmFAL6UkAD0iw4aCNQ7wAKki0BGOAF44ACgCUxgHw8+AojACuUanAA8AE2AA3K3S1uAPRevuTy5Eoq8OqaOnowAEzGZmDsHGJsEJyWRja8AnDAmClpAHQclvkF9khOLu4hVtypWRzlskGNdvzyBQ7OrtSOADbDYQrK0NEaWnC6WgDMyaYtnBlp5mLI6DCl2xgAchAezLm2BUUlreWV3dX99Z4+TattHB3Bz3e9NXWDI2NwpEpmoZnEtAAWZavdatTaIYi7fYwACiwyQICQtDgAB84ENRtZzgJLisyhVidU4A9XE9fM1yR8ugUfvwafiAXJKJQYrNMBAIMkAEIC9EoaimfTmCjkNDDFAcLjzGAAVjgSjo1A8XGRpQAwrhINQsfAqmysScoBYtoi9oi0RiTZTqqT+RBLOzTHS4IErNK7n1agMOaMKCy5DK5QqlfEAGzqxSa7UInb6w0aJ1m6kW+jWlMYO07B2Y7F4gnDZ0XYqmN0eoP1L0hH1+sNU7N-EPDVs9CM8sFwPUACyQaAA1sscnk7p6AwI3FZZ1S3MrDL7F9Vl-Ekmu2xvlUsd7u58qoYej+5lWqz0fN1p49eCkE-eNyEA) +To address the issue, we remove the function component return type annotations and allow TypeScript to infer the return types. +```ts +const App = ({ message }: AppProps) =>
{message}
; +``` + ## {\[KeyType\]: ValueType} -> Partial> Object index types work pretty differently between Flow and TypeScript. Flow allows any type to be an index, while TypeScript supports only strings and numbers. One other difference I found is Flow marks all of the keys of the object as optional by default. To get this same behavior, we have to wrap the object in `Partial`. @@ -83,7 +88,7 @@ All of the types are imported and namespaced, and the codemod automatically adds ## React.AbstractComponent -[AbstractComponent](https://flow.org/en/docs/react/types/#toc-react-abstractcomponent) is a Flow type that helps you construct higher order components, and there is no direct TypeScript equivalent. The identical type is a `ComponentType` with the type of `ref` modified. That is a lot more verbose, so we added the `Flow.AbstractComponent` utility to keep things concise. +[AbstractComponent](https://flow.org/en/docs/react/types/#toc-react-abstractcomponent) is a Flow type that helps you construct higher order components, and there is no direct TypeScript equivalent. We remove the annotations of `AbstractComponent` and allow TypeScript to infer them. ## export type * from './foo` @@ -94,11 +99,11 @@ In many cases, it's ideal to keep `type` imports when TypeScript supports it. Th We've encountered some global Flow types like [`TimeoutID`](https://github.com/facebook/flow/issues/5627) that needed conversions. -## React.ElementConfig -> JSX.LibraryManagedAttributes +## React.ElementConfig -> React.ComponentProps -Flow has a utility type which gets the props from a component. [TypeScript has a similar type](https://github.com/Microsoft/TypeScript/issues/26704) called `JSX.LibraryManagedAttributes` but there's some extra conversion needed to make the type parameters work. +Flow has a utility type which gets the props from a component. TypeScript has a similar type called `ComponentProps`. -[TS Playground example](https://www.TypeScriptlang.org/play?ts=4.4.2#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wG4BYAKCuADsYkpN0k4AFHMAZzgG8q4cGAE8ANowBccAEYQI4lLQqUAvlSppRKLjwAqSLvCQAPBrQAmPZOhgA6AMK5ItJPQA8HCNwB8fAXDQIWkMoAFcMaAAKME4uKU9uAEo-SkFBLlCwRmjYxOVBNWpUuCILbMSpawxbKpgAOQhzVn5iwSIYUKhaOEj-NLg3c2AANzgAem8+uDz-QsL1SjoGJhY4R3Ag13gWwRivLgB+KUVhZXnKESy4fUMEngBeOAApAGUADVsAGWBpKBQoYQAWUUKAA5khzABBGAwKA-UIMLhuG4wAA01wMMAA2gAiPbcHEAXW8ykurF0YkYcEeKLuWPwInEUHwhNIQA) +[TS Playground example](https://www.typescriptlang.org/play?ts=4.4.2#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgcilQ3wG4BYAKCuADsYkpN0k4AFHMAZzgG8q4cGAE8ANowBccAEYQI4lLQqUAvlSppRKLjwAqSLvCQAPBrQAmPZOhgA6AMK5ItJPQA8HCNwB8fAXDQIWkMoAFcMaAAKME4uKU9uAEo-SkFBLlCwRmjYxOVBNWpUuCILbMSpawxbKpgAOQhzVn5iwSIYUKhaOEj-NLg3c2AANzgAem8+uDz-QsL1SjoGJhY4R3Ag13gWwRivLgB+KUVhZXnKESy4fUMEngBeRGI7dectu7dLpAhMa4MYbzKL7XMSMOCPG4wO4AbXwInEUHwAF1SEA) ## Utility types diff --git a/package.json b/package.json index 062a263..185cd15 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "jest-junit": "^12.0.0", "patch-package": "^6.4.7", "prettier": "^2.4.1", - "recast": "^0.20.4", + "recast": "0.23.4", "signale": "^1.4.0", "ts-jest": "^26.0.0", "ts-morph": "^13.0.2", diff --git a/patches/recast+0.20.4.patch b/patches/recast+0.20.4.patch deleted file mode 100644 index 5af2827..0000000 --- a/patches/recast+0.20.4.patch +++ /dev/null @@ -1,17 +0,0 @@ -diff --git a/node_modules/recast/lib/printer.js b/node_modules/recast/lib/printer.js -index 8cbc392..470b3c4 100644 ---- a/node_modules/recast/lib/printer.js -+++ b/node_modules/recast/lib/printer.js -@@ -1730,8 +1730,12 @@ function genericPrintNoParens(path, options, print) { - } - case "TSTypeParameterDeclaration": - case "TSTypeParameterInstantiation": -+ // If the first parameter has a comment, we want to insert a new line to avoid causing a syntax error: -+ const parameterNode = path.getValue() -+ const [firstParam] = parameterNode.params || []; - return lines_1.concat([ - "<", -+ firstParam && firstParam.comments && firstParam.comments.length ? lines_1.fromString("\n") : lines_1.fromString(""), - lines_1.fromString(", ").join(path.map(print, "params")), - ">", - ]); diff --git a/src/convert/add-imports.ts b/src/convert/add-imports.ts deleted file mode 100644 index 16bfdfe..0000000 --- a/src/convert/add-imports.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as t from "@babel/types"; -import traverse from "@babel/traverse"; -import { TransformerInput } from "./transformer"; - -/** - * If any of the transforms used a utility type, we need to import them - * @param state - * @param file - */ -export function addImports({ state, file }: TransformerInput) { - traverse(file, { - Program: { - exit(path) { - if (state.usedUtils) { - const importDeclaration = t.importDeclaration( - [t.importSpecifier(t.identifier("Flow"), t.identifier("Flow"))], - t.stringLiteral("flow-to-typescript-codemod") - ); - path.node.body = [importDeclaration, ...path.node.body]; - } - }, - }, - }); -} diff --git a/src/convert/declarations.test.ts b/src/convert/declarations.test.ts index 5874f97..45589c4 100644 --- a/src/convert/declarations.test.ts +++ b/src/convert/declarations.test.ts @@ -107,6 +107,12 @@ describe("transform declarations", () => { expectMigrationReporterMethodNotCalled(`importWithExtension`); }); + it("drops React.AbstractComponent imports", async () => { + const src = `import {type AbstractComponent, forwardRef} from 'react';`; + const expected = `import {forwardRef} from 'react';`; + expect(await transform(src)).toBe(expected); + }); + describe("Flow to TypeScript React import transformations", () => { Object.entries(ReactTypes).forEach(([flowType, tsType]) => { it(`transforms type imports of ${flowType} from react`, async () => { @@ -168,6 +174,48 @@ describe("transform declarations", () => { }); }); }); + + describe("change react-router-dom Location and RouterHistory imports", () => { + it("keep react-router-dom", async () => { + const src = dedent(` + import {type Location as L, type RouterHistory as H, useLocation} from 'react-router-dom'; + const location: L | null = null; + const history: H | null = null; + `); + const expected = dedent(` + import {History as H, Location as L} from 'history'; + import {useLocation} from 'react-router-dom'; + const location: L | null = null; + const history: H | null = null; + `); + expect(await transform(src)).toBe(expected); + }); + + it("remove react-router-dom", async () => { + const src = dedent(` + import {type Location as L, type RouterHistory as H} from 'react-router-dom'; + const location: L | null = null; + const history: H | null = null; + `); + const expected = dedent(` + import {History as H, Location as L} from 'history'; + const location: L | null = null; + const history: H | null = null; + `); + expect(await transform(src)).toBe(expected); + }); + + it("do not insert history if one has already been found", async () => { + const src = dedent(` + import {type RouterHistory} from 'react-router-dom'; + import {createMemoryHistory} from 'history'; + `); + const expected = dedent(` + import {History as RouterHistory, createMemoryHistory} from 'history'; + `); + expect(await transform(src)).toBe(expected); + }); + }); }); /* @@ -276,7 +324,7 @@ describe("transform declarations", () => { it("converts more complicated $Exact types", async () => { const src = dedent`type Test = $Exact;`; const expected = dedent`type Test = T | { - foo: string + foo: string; };`; expect(await transform(src)).toBe(expected); }); @@ -504,7 +552,7 @@ describe("transform declarations", () => { it("Converts React.Node to React.ReactNode in Props", async () => { const src = `type Props = {children?: React.Node};`; const expected = dedent`type Props = { - children?: React.ReactNode + children?: React.ReactNode; };`; expect(await transform(src)).toBe(expected); }); @@ -519,17 +567,7 @@ describe("transform declarations", () => { expect(await transform(src)).toBe(expected); }); - it("Converts React.Node to React.ReactElement in render", async () => { - const src = dedent`class Foo extends React.Component { - render(): React.Node {return
}; - };`; - const expected = dedent`class Foo extends React.Component { - render(): React.ReactElement {return
}; - };`; - expect(await transform(src)).toBe(expected); - }); - - it("Adds null to React.ReactElement in render", async () => { + it("Converts React.Node to ReactNode in render", async () => { const src = dedent`class Foo extends React.Component { render(): React.Node { if (foo) return (
); @@ -537,7 +575,7 @@ describe("transform declarations", () => { }; };`; const expected = dedent`class Foo extends React.Component { - render(): React.ReactElement | null { + render(): ReactNode { if (foo) return (
); return null; }; @@ -545,17 +583,17 @@ describe("transform declarations", () => { expect(await transform(src)).toBe(expected); }); - it("Converts React.Node to React.ReactElement for render in arrow", async () => { + it("Strips out React.Node for render in arrow", async () => { const src = dedent`class Foo extends React.Component { render = (): React.Node => {return
}; };`; const expected = dedent`class Foo extends React.Component { - render = (): React.ReactElement => {return
}; + render = () => {return
}; };`; expect(await transform(src)).toBe(expected); }); - it("Does not convert React.Node to React.ReactElement in non-render", async () => { + it("Does not convert React.Node to ReactNode in non-render", async () => { const src = dedent`class Foo extends React.Component { rendering(): React.Node {return
}; };`; @@ -565,6 +603,19 @@ describe("transform declarations", () => { expect(await transform(src)).toBe(expected); }); + it("removes React.AbstractComponent<>", async () => { + const src = dedent` + // @flow + const C1: AbstractComponent = memo((props) => null); + const C2: AbstractComponent = memo(Comp); + `; + const expected = dedent` + const C1 = memo((props) => null); + const C2 = memo(Comp); + `; + expect(await transform(src)).toBe(expected); + }); + describe("untyped usestate", () => { it("Applies any and creates warning for untyped empty useState.", async () => { const src = `const test = React.useState();`; @@ -739,21 +790,6 @@ describe("transform declarations", () => { `; expect(await transform(src)).toBe(expected); }); - it("when a comment is in a type param declaration, it should preserve the newline", async () => { - const src = dedent` - const AThing: Array< - // FlowFixMe - number> = [] - `; - - const expected = dedent` - const AThing: Array< - // FlowFixMe - number> = [] - `; - - expect(await transform(src)).toBe(expected); - }); }); describe("for opaque types", () => { diff --git a/src/convert/declarations.ts b/src/convert/declarations.ts index 56bf5b7..bf70bea 100644 --- a/src/convert/declarations.ts +++ b/src/convert/declarations.ts @@ -62,6 +62,36 @@ const updateReactImports = ( } }; +/** + * Rename React Router imports for TypeScript + */ +const updateReactRouterImports = ( + node: t.ImportDeclaration, + specifier: t.ImportSpecifier +) => { + if ( + node.source.value === "react-router-dom" && + (specifier.importKind === "type" || node.importKind === "type") + ) { + // `import type {Match} from 'react-router-dom'` => `import {match} from 'react-router-dom'` + if ( + specifier.type === "ImportSpecifier" && + specifier.imported.type === "Identifier" && + specifier.imported.name === "Match" + ) { + specifier.imported.name = "match"; + } + // `import {type Match} from 'react-router-dom'` => `import {match} from 'react-router-dom'` + if ( + specifier.type === "ImportSpecifier" && + specifier.local.type === "Identifier" && + specifier.local.name === "Match" + ) { + specifier.local.name = "match"; + } + } +}; + export function transformDeclarations({ reporter, state, @@ -106,6 +136,7 @@ export function transformDeclarations({ (specifier.importKind === "type" || path.node.importKind === "type") ) { updateReactImports(path.node, specifier); + updateReactRouterImports(path.node, specifier); // `import {type X} from` => `import {X} from` if (specifier.importKind === "type") { @@ -114,6 +145,18 @@ export function transformDeclarations({ } } + // `import {type AbstractComponent, ...} from 'react'` => `import {...} from 'react'` + if (path.node.source.value === "react") { + path.node.specifiers = path.node.specifiers.filter( + (s) => + !( + s.type === "ImportSpecifier" && + s.imported.type === "Identifier" && + s.imported.name === "AbstractComponent" + ) + ); + } + return; } @@ -260,6 +303,19 @@ export function transformDeclarations({ }, VariableDeclarator(path) { + // `const c: AbstractComponent =` → `const c =` + if ( + path.node.id.type === "Identifier" && + path.node.id.typeAnnotation?.type === "TypeAnnotation" && + path.node.id.typeAnnotation.typeAnnotation.type === + "GenericTypeAnnotation" && + path.node.id.typeAnnotation.typeAnnotation.id.type === "Identifier" && + path.node.id.typeAnnotation.typeAnnotation.id.name === + "AbstractComponent" + ) { + path.node.id.typeAnnotation = null; + } + if ( path.parent.type === "VariableDeclaration" && path.parentPath.parent.type !== "ForStatement" && diff --git a/src/convert/expressions.test.ts b/src/convert/expressions.test.ts index 3f23449..5acc474 100644 --- a/src/convert/expressions.test.ts +++ b/src/convert/expressions.test.ts @@ -126,7 +126,7 @@ describe("transform expressions", () => { |}> = new Array(0);`; const expected = dedent` const a: Array<{ - foo: 'bar' + foo: 'bar'; }> = new Array(0);`; expect(await transform(src)).toBe(expected); }); @@ -140,18 +140,23 @@ describe("transform expressions", () => { const expected = dedent` const test = () => { return class extends React.Component, { - bar: string + bar: string; }> {}; };`; expect(await transform(src)).toBe(expected); }); - it("should not change if there are no exact bars", async () => { - const expected = dedent` + it("should only add semicolon if there are no exact bars", async () => { + const src = dedent` + // @flow const a: Array<{ foo: 'bar' }> = new Array(0);`; - expect(await transform(expected)).toBe(expected); + const expected = dedent` + const a: Array<{ + foo: 'bar'; + }> = new Array(0);`; + expect(await transform(src)).toBe(expected); }); it("should remove the exact object types from constructed objects", async () => { @@ -161,7 +166,7 @@ describe("transform expressions", () => { |}>();`; const expected = dedent` const a = new Array<{ - foo: 'bar' + foo: 'bar'; }>();`; expect(await transform(src)).toBe(expected); }); @@ -215,6 +220,69 @@ describe("transform expressions", () => { }); }); + describe("React.AbstractComponent", () => { + it("should handle anonymous component", async () => { + const src = dedent` + // @flow + export default (memo((props) => null): AbstractComponent); + `; + const expected = dedent` + export default memo((props) => null); + `; + expect(await transform(src)).toBe(expected); + }); + + it("should handle named component", async () => { + const src = dedent` + // @flow + export default (memo(Comp): AbstractComponent); + `; + const expected = dedent` + export default memo(Comp); + `; + expect(await transform(src)).toBe(expected); + }); + + it("should not remove when annotation is not for a function call", async () => { + const src = dedent` + export default function withFoo(f: Foo):

(S: AbstractComponent

) => AbstractComponent

{ + return function withFoo

(Subject: AbstractComponent

): AbstractComponent

{ + return () => null; + }; + } + `; + expect(await transform(src)).toBe(src); + }); + }); + + describe("React.forwardRef", () => { + it("should handle anonymous component", async () => { + const src = dedent` + // @flow + const C: AbstractComponent = forwardRef((props, ref) => null); + export default (forwardRef((props, ref) => null): AbstractComponent); + `; + const expected = dedent` + const C = forwardRef((props, ref) => null); + export default forwardRef((props, ref) => null); + `; + expect(await transform(src)).toBe(expected); + }); + + it("should handle named component", async () => { + const src = dedent` + // @flow + const C: AbstractComponent = forwardRef(Comp); + export default (forwardRef(Comp): AbstractComponent); + `; + const expected = dedent` + const C = forwardRef(Comp); + export default forwardRef(Comp); + `; + expect(await transform(src)).toBe(expected); + }); + }); + describe.each(JEST_MOCK_METHODS)("jest.%s paths", (mockMethod) => { it("should do nothing if there is no extension already", async () => { const src = dedent`jest.${mockMethod}('foo');`; diff --git a/src/convert/expressions.ts b/src/convert/expressions.ts index 0e3b3cc..1c7e618 100644 --- a/src/convert/expressions.ts +++ b/src/convert/expressions.ts @@ -168,6 +168,15 @@ export function transformExpressions({ ); } }, + TSAsExpression(path) { + if ( + path.node.typeAnnotation.type === "TSTypeReference" && + path.node.typeAnnotation.typeName.type === "Identifier" && + path.node.typeAnnotation.typeName.name === "AbstractComponent" + ) { + path.replaceWith(path.node.expression); + } + }, ArrowFunctionExpression(path) { // Arrow functions with a generic type parameter (() => {}) often don't typecheck in tsx files // since they can be parsed as a JSX tag. To solve this the type parameters usually extend unknown, @@ -188,6 +197,19 @@ export function transformExpressions({ CallExpression(path) { migrateArgumentsToParameters(path, reporter, state); + // forwardRef<> in TS has the type parameters in reverse order + // `forwardRef` → `forwardRef` + if ( + path.node.type === "CallExpression" && + path.node.callee.type === "Identifier" && + path.node.callee.name === "forwardRef" && + path.node.typeParameters?.type === "TSTypeParameterInstantiation" + ) { + path.node.typeParameters.params = path.node.typeParameters.params + .concat() + .reverse(); + } + if ( t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.property) && diff --git a/src/convert/jsx-spread/jsx-spread.test.ts b/src/convert/jsx-spread/jsx-spread.test.ts index 599b2ed..821fd4b 100644 --- a/src/convert/jsx-spread/jsx-spread.test.ts +++ b/src/convert/jsx-spread/jsx-spread.test.ts @@ -22,8 +22,8 @@ describe("transform spread JSX attributes", () => { const expected = ` import {Flow} from 'flow-to-typescript-codemod'; type Props = { - it: string, - foo: number + it: string; + foo: number; }; function Foobar(x: Props & Omit, 'it'>, keyof Props>) { @@ -51,8 +51,8 @@ describe("transform spread JSX attributes", () => { const expected = ` import {Flow} from 'flow-to-typescript-codemod'; type Props = { - it: string, - foo: number + it: string; + foo: number; }; const Foobar = (x: Props & Omit, 'it'>, keyof Props>) => { @@ -84,8 +84,8 @@ describe("transform spread JSX attributes", () => { const expected = ` import {Flow} from 'flow-to-typescript-codemod'; type Props = { - it: string, - foo: number + it: string; + foo: number; }; function Foobar(x: Props & Omit, 'it'>, keyof Props>) { @@ -121,8 +121,8 @@ describe("transform spread JSX attributes", () => { const expected = ` import {Flow} from 'flow-to-typescript-codemod'; type Props = { - it: string, - foo: number + it: string; + foo: number; }; class MyComponent extends React.Component, 'it'>, keyof Props>> { @@ -157,8 +157,8 @@ describe("transform spread JSX attributes", () => { const expected = ` import {Flow} from 'flow-to-typescript-codemod'; type Props = { - it: string, - foo: number + it: string; + foo: number; }; class MyComponent extends React.Component, 'it'>, keyof Props>> { @@ -192,8 +192,8 @@ describe("transform spread JSX attributes", () => { const expected = ` type Props = { - it: string, - foo: number + it: string; + foo: number; }; class MyComponent extends React.Component { @@ -228,11 +228,11 @@ describe("transform spread JSX attributes", () => { const expected = ` import {Flow} from 'flow-to-typescript-codemod'; type State = { - thing: boolean + thing: boolean; }; type Props = { - it: string, - foo: number + it: string; + foo: number; }; class MyComponent extends React.Component, 'it'>, keyof Props>, State> { @@ -263,8 +263,8 @@ describe("transform spread JSX attributes", () => { const expected = ` import {Flow} from 'flow-to-typescript-codemod'; type Props = { - it: string, - foo: number + it: string; + foo: number; }; function Foobar(x: Props & Omit, keyof Props>) { @@ -292,8 +292,8 @@ describe("transform spread JSX attributes", () => { }`; const expected = ` type Props = { - it: string, - foo: number + it: string; + foo: number; }; function Foobar(x: Props) { diff --git a/src/convert/migrate/type.ts b/src/convert/migrate/type.ts index 0d1e325..8f4593f 100644 --- a/src/convert/migrate/type.ts +++ b/src/convert/migrate/type.ts @@ -4,7 +4,6 @@ import { inheritLocAndComments, GlobalTypes, LiteralTypes, - hasNullReturn, } from "../utils/common"; import { migrateTypeParameterDeclaration, @@ -18,10 +17,12 @@ import { MomentTypes, } from "../utils/type-mappings"; import { State } from "../../runner/state"; -import { matchesFullyQualifiedName } from "../utils/matchers"; +import { matchesReact } from "../utils/matchers"; import { migrateFunctionParameters } from "./function-parameter"; import { MetaData } from "./metadata"; +export const REMOVE_ME_ID = "__RemoveMe__"; + export function migrateType( reporter: MigrationReporter, state: State, @@ -144,6 +145,26 @@ function actuallyMigrateType( return t.tsTypeReference(t.identifier("ReadonlyArray"), params); } + // `$ReadOnlySet` → `ReadonlySet` + if ( + id.type === "Identifier" && + id.name === "$ReadOnlySet" && + params && + params.params.length === 1 + ) { + return t.tsTypeReference(t.identifier("ReadonlySet"), params); + } + + // `$ReadOnlyMap` → `ReadonlyMap` + if ( + id.type === "Identifier" && + id.name === "$ReadOnlyMap" && + params && + params.params.length === 2 + ) { + return t.tsTypeReference(t.identifier("ReadonlyMap"), params); + } + // `$ReadOnly` → `Readonly` if ( id.type === "Identifier" && @@ -178,10 +199,10 @@ function actuallyMigrateType( return t.tsIndexedAccessType(params.params[0], typeOperator); } - // `$Shape` → `Partial` + // `$Shape|$Partial` → `Partial` if ( id.type === "Identifier" && - id.name === "$Shape" && + (id.name === "$Shape" || id.name === "$Partial") && params && params.params.length === 1 ) { @@ -536,29 +557,24 @@ function actuallyMigrateType( } } if ( - ((matchesFullyQualifiedName("React", "Node")(id) && + (((matchesReact("Node")(id) || matchesReact("ReactNode")(id)) && isRenderMethodOrNonClassMethodReturnType) || - matchesFullyQualifiedName("React", "MixedElement")(id)) && + matchesReact("MixedElement")(id)) && !params ) { - const parentNode = metaData?.path?.parentPath?.node; - let hasNull = false; - if (parentNode && "body" in parentNode) { - const parentPath = metaData?.path?.parentPath; - const scope = metaData?.path?.scope; - hasNull = hasNullReturn( - parentNode.body as t.BlockStatement, - scope, - parentPath - ); - } - const reactElement = t.tsTypeReference( - t.tsQualifiedName(t.identifier("React"), t.identifier("ReactElement")) - ); - if (hasNull) { - return t.tsUnionType([reactElement, t.tsNullKeyword()]); + const parentType = metaData?.path?.parentPath?.type; + const parentReturnType = + // @ts-expect-error returnType not found + metaData?.path?.parentPath?.node?.returnType?.typeAnnotation?.id + ?.name; + if ( + (parentType === "FunctionDeclaration" || + parentType === "ArrowFunctionExpression") && + parentReturnType !== "$ReadOnlyArray" + ) { + return t.tsTypeReference(t.identifier(REMOVE_ME_ID)); } else { - return reactElement; + return t.tsTypeReference(t.identifier("ReactNode")); } } @@ -567,7 +583,7 @@ function actuallyMigrateType( // convert this flow type to something that TypeScript understands so we can get the less strict children // prop checking. if ( - matchesFullyQualifiedName("React", "ChildrenArray")(id) && + matchesReact("ChildrenArray")(id) && params && params.params.length === 1 ) { @@ -597,33 +613,17 @@ function actuallyMigrateType( ); } - // React.ElementConfig -> JSX.LibraryManagedAttributes> + // ElementConfig -> ComponentProps if ( - id.type === "TSQualifiedName" && - id.left.type === "Identifier" && - id.left.name === "React" && - id.right.type === "Identifier" && - id.right.name === "ElementConfig" && + matchesReact("ElementConfig")(id) && params && params.params.length === 1 ) { const parameter = params.params[0]; return t.tsTypeReference( - t.tsQualifiedName( - t.identifier("JSX"), - t.identifier("LibraryManagedAttributes") - ), - t.tsTypeParameterInstantiation([ - parameter, - t.tsTypeReference( - t.tsQualifiedName( - t.identifier("React"), - t.identifier("ComponentProps") - ), - t.tsTypeParameterInstantiation([parameter]) - ), - ]) + t.identifier("ComponentProps"), + t.tsTypeParameterInstantiation([parameter]) ); } @@ -694,11 +694,25 @@ function actuallyMigrateType( } } + // `Portal/ElementProps/Etc` → `ReactPortal/ComponentProps/Etc` + // NOTE: skipping Node, Child, Children, Text, Fragment, FragmentType because the names are too + // generic and we don't want to accidentally convert the wrong thing. For example, we don't want + // to covert Node to ReactNode because Node is also a global builtin type type for DOM Nodes. + if ( + id.type === "Identifier" && + ["Portal", "ElementProps", "StatelessFunctionalComponent"].includes( + id.name + ) + ) { + return t.tsTypeReference( + t.identifier(ReactTypes[id.name as keyof typeof ReactTypes]), + params + ); + } + // `React.Portal/Children/Etc` → `React.ReactPortal/ReactChildren/Etc` if ( id.type === "TSQualifiedName" && - id.left.type === "Identifier" && - id.left.name === "React" && id.right.type === "Identifier" && id.right.name in ReactTypes ) { @@ -736,9 +750,27 @@ function actuallyMigrateType( ); } + if (id.type === "Identifier" && id.name === "Match") { + return t.tsTypeReference( + t.identifier("match"), + t.tsTypeParameterInstantiation([ + t.tsTypeReference( + t.identifier("{ [key: string]: string | undefined }") + ), + ]) + ); + } + return t.tsTypeReference(id, params); } + // `T[K]` → `T[K]` + case "IndexedAccessType": + return t.tsIndexedAccessType( + migrateType(reporter, state, flowType.objectType), + migrateType(reporter, state, flowType.indexType) + ); + case "InterfaceTypeAnnotation": throw new Error(`Unsupported AST node: ${JSON.stringify(flowType.type)}`); @@ -762,8 +794,11 @@ function actuallyMigrateType( return t.tsNeverKeyword(); case "NullableTypeAnnotation": { + const type = migrateType(reporter, state, flowType.typeAnnotation); return t.tsUnionType([ - migrateType(reporter, state, flowType.typeAnnotation), + flowType.typeAnnotation.type === "FunctionTypeAnnotation" + ? t.tsParenthesizedType(type) + : type, t.tsNullKeyword(), t.tsUndefinedKeyword(), ]); @@ -880,10 +915,27 @@ function actuallyMigrateType( if (types.length === 1) { return types[0]; } else { - return t.tsIntersectionType(types); + return t.tsParenthesizedType(t.tsIntersectionType(types)); } } + // `T?.[K]` → `NonNullable[K] | null | undefined` + case "OptionalIndexedAccessType": { + return t.tsUnionType([ + t.tsIndexedAccessType( + t.tsTypeReference( + t.identifier("NonNullable"), + t.tsTypeParameterInstantiation([ + migrateType(reporter, state, flowType.objectType), + ]) + ), + migrateType(reporter, state, flowType.indexType) + ), + t.tsNullKeyword(), + t.tsUndefinedKeyword(), + ]); + } + case "StringLiteralTypeAnnotation": return t.tsLiteralType(t.stringLiteral(flowType.value)); @@ -961,6 +1013,7 @@ function actuallyMigrateType( const never: { type: string } = flowType; reporter.unhandledFlowInputNode( state.config.filePath, + // @ts-expect-error loc not found flowType.loc as t.SourceLocation, (flowType as unknown as { name: string }).name ?? "undefined", JSON.stringify(never.type) diff --git a/src/convert/move-imports.test.ts b/src/convert/move-imports.test.ts new file mode 100644 index 0000000..f2664a7 --- /dev/null +++ b/src/convert/move-imports.test.ts @@ -0,0 +1,59 @@ +import dedent from "dedent"; +import { transform } from "./utils/testing"; + +jest.mock("../runner/migration-reporter/migration-reporter.ts"); +jest.mock("./flow/type-at-pos.ts"); + +describe("imports", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("move react-router-dom imports", () => { + it("react-router-dom stays and history stays", async () => { + const src = dedent(` + import {type RouterHistory as H, useLocation} from 'react-router-dom'; + import {createMemoryHistory} from 'history'; + `); + const expected = dedent(` + import {useLocation} from 'react-router-dom'; + import {History as H, createMemoryHistory} from 'history'; + `); + expect(await transform(src)).toBe(expected); + }); + + it("react-router-dom stays and history is added", async () => { + const src = dedent(` + import {type RouterHistory as H, useLocation} from 'react-router-dom'; + `); + const expected = dedent(` + import {History as H} from 'history'; + import {useLocation} from 'react-router-dom'; + `); + expect(await transform(src)).toBe(expected); + }); + + it("react-router-dom is removed and history stays", async () => { + const src = dedent(` + import {type RouterHistory as H} from 'react-router-dom'; + import {createMemoryHistory} from 'history'; + `); + const expected = dedent(` + import {History as H, createMemoryHistory} from 'history'; + `); + expect(await transform(src)).toBe(expected); + }); + + it("react-router-dom is removed and history is added", async () => { + const src = dedent(` + import {type RouterHistory as H} from 'react-router-dom'; + const h: H | null = null; + `); + const expected = dedent(` + import {History as H} from 'history'; + const h: H | null = null; + `); + expect(await transform(src)).toBe(expected); + }); + }); +}); diff --git a/src/convert/move-imports.ts b/src/convert/move-imports.ts new file mode 100644 index 0000000..1b7d581 --- /dev/null +++ b/src/convert/move-imports.ts @@ -0,0 +1,118 @@ +import * as t from "@babel/types"; +import traverse from "@babel/traverse"; +import { TransformerInput } from "./transformer"; + +/** + * Add or remove imports + * @param state + * @param file + */ +export function moveImports({ state, file }: TransformerInput) { + traverse(file, { + Program: { + exit(path) { + const removeImportDeclaration = ( + importDeclaration: t.ImportDeclaration + ) => { + path.node.body = path.node.body.filter( + (node) => + !( + t.isImportDeclaration(node) && + node.source.value === importDeclaration.source.value + ) + ); + }; + + const insertImportDeclaration = ( + importDeclaration: t.ImportDeclaration + ) => { + path.node.body = [importDeclaration, ...path.node.body]; + }; + + if (state.usedUtils) { + const importDeclaration = t.importDeclaration( + [t.importSpecifier(t.identifier("Flow"), t.identifier("Flow"))], + t.stringLiteral("flow-to-typescript-codemod") + ); + insertImportDeclaration(importDeclaration); + } + + const importDeclarations = path.node.body.reduce< + Array + >( + (agg, node) => + t.isImportDeclaration(node) ? agg.concat([node]) : agg, + [] + ); + const reactRouterDomImportDeclaration: t.ImportDeclaration | undefined = + importDeclarations.find( + (node) => node.source.value === "react-router-dom" + ); + + if (reactRouterDomImportDeclaration) { + const locationLocalName: string | undefined = + reactRouterDomImportDeclaration.specifiers.find( + (s) => + s.type === "ImportSpecifier" && + s.imported.type === "Identifier" && + s.imported.name === "Location" + )?.local.name; + const historyLocalName: string | undefined = + reactRouterDomImportDeclaration.specifiers.find( + (s) => + s.type === "ImportSpecifier" && + s.imported.type === "Identifier" && + s.imported.name === "RouterHistory" + )?.local.name; + + const newImportSpecifiers = [ + !!historyLocalName && + t.importSpecifier( + t.identifier(historyLocalName), + t.identifier("History") + ), + !!locationLocalName && + t.importSpecifier( + t.identifier(locationLocalName), + t.identifier("Location") + ), + ].filter(Boolean) as t.ImportSpecifier[]; + + if (newImportSpecifiers.length > 0) { + reactRouterDomImportDeclaration.specifiers = + reactRouterDomImportDeclaration.specifiers.filter( + (s) => + !( + s.type === "ImportSpecifier" && + s.imported.type === "Identifier" && + (s.imported.name === "Location" || + s.imported.name === "RouterHistory") + ) + ); + if (reactRouterDomImportDeclaration.specifiers.length === 0) { + removeImportDeclaration(reactRouterDomImportDeclaration); + } + + const historyImportDeclaration = importDeclarations.find( + (node) => node.source.value === "history" + ); + + if (historyImportDeclaration) { + historyImportDeclaration.specifiers = [ + ...newImportSpecifiers, + ...historyImportDeclaration.specifiers, + ]; + } else { + insertImportDeclaration( + t.importDeclaration( + newImportSpecifiers, + t.stringLiteral("history") + ) + ); + } + } + } + }, + }, + }); +} diff --git a/src/convert/private-types.test.ts b/src/convert/private-types.test.ts index 197719f..a73914a 100644 --- a/src/convert/private-types.test.ts +++ b/src/convert/private-types.test.ts @@ -17,7 +17,7 @@ describe("transform type annotations", () => { it("converts React$Element", async () => { const src = `const Component = (props: Props): React$Element => {return

};`; - const expected = `const Component = (props: Props): React.Element => {return
};`; + const expected = `const Component = (props: Props): React.ReactElement => {return
};`; expect(await transform(src)).toBe(expected); }); diff --git a/src/convert/private-types.ts b/src/convert/private-types.ts index ff45e41..2bad923 100644 --- a/src/convert/private-types.ts +++ b/src/convert/private-types.ts @@ -3,6 +3,11 @@ import traverse from "@babel/traverse"; import { replaceWith } from "./utils/common"; import { TransformerInput } from "./transformer"; +const isRelayEmittedType = (name: string): boolean => + ["fragmentType", "data", "key", "variables"].some((relayTypeSuffix) => + name.endsWith(relayTypeSuffix) + ); + /** * Flow commonly uses `$` to denote private type members like React$Node. * This syntax is hard to account for everywhere, so we convert it to `.` at the start. @@ -16,7 +21,10 @@ export function transformPrivateTypes({ Identifier(path) { const id = path.node; const hasPrivateType = - /\w\$\w/.test(id.name) && !state.config.keepPrivateTypes; + /\w\$\w/.test(id.name) && + !isRelayEmittedType(id.name) && + !id.name.startsWith("$IMPORTED_TYPE$") && + !state.config.keepPrivateTypes; const privateReactType = id.name.startsWith("React$"); const privateFlowType = id.name.startsWith("$FlowFixMe"); const isTypeAnnotation = path.parentPath.type === "GenericTypeAnnotation"; diff --git a/src/convert/remove-flow-comments.test.ts b/src/convert/remove-flow-comments.test.ts index 8a6ae76..8cc031b 100644 --- a/src/convert/remove-flow-comments.test.ts +++ b/src/convert/remove-flow-comments.test.ts @@ -25,6 +25,22 @@ describe("remove-flow-pragmas", () => { expect(await transform(src)).toEqual(expected); }); + it("should retain the rest of the comment", async () => { + const src = dedent` + /** + * @flow + * @jest-environment jsdom + */ + ${standardProgram} + `; + + expect(await transform(src)).toEqual(dedent` + /** + * @jest-environment jsdom + */ + ${expected}`); + }); + it("should remove the comment even if it has extra comment marks", async () => { const src = dedent` // ///// / / / / / /// // // /// ////////// /// // /////// @flow diff --git a/src/convert/remove-flow-comments.ts b/src/convert/remove-flow-comments.ts index 2c3769b..9017044 100644 --- a/src/convert/remove-flow-comments.ts +++ b/src/convert/remove-flow-comments.ts @@ -3,20 +3,49 @@ import * as t from "@babel/types"; import { types } from "recast"; import { TransformerInput } from "./transformer"; +const ESLINT_FLOW_RULE_PREFIX = "flowtype/"; + const flowComments = [ "@flow", "$FlowFixMe", "$FlowIssue", "$FlowExpectedError", "$FlowIgnore", + ESLINT_FLOW_RULE_PREFIX, // ESLint flowtype rules ]; +/*** + * Remove flowtype/ rules from eslint-disable-line or eslint-disable-next-line comments, + * unless flowtype/ rules are the only rules. + * + * If flowtype/ rules are the only rules, the entire comment is removed. + */ +const stripOutFlowESLintRuleFromNonFlowRules = (comment: string): string => { + const matches = comment.match( + /^(.*)(eslint-disable-line|eslint-disable-next-line)\s+(.*)+/ + ); + if (!matches) { + return comment; + } + const [, prefix, eslintDirective, rulesString] = matches; + if ( + rulesString.includes(ESLINT_FLOW_RULE_PREFIX) && + rulesString.includes(",") + ) { + const rules = rulesString.split(",").map((rule) => rule.trim()); + if (rules.some((rule) => !rule.startsWith(ESLINT_FLOW_RULE_PREFIX))) { + return `${prefix}${eslintDirective} ${rules + .filter((rule) => !rule.startsWith(ESLINT_FLOW_RULE_PREFIX)) + .join(",")}`; + } + } + return comment; +}; + /** * Scan through top level programs, or code blocks and remove Flow-specific comments */ -const removeComments = ( - path: NodePath | NodePath -) => { +const removeTopLevelComments = (path: NodePath) => { if (path.node.body.length === 0) { return; } @@ -28,10 +57,7 @@ const removeComments = ( rootNode.comments = comments - ?.filter( - (comment) => !flowComments.some((c) => comment.value.includes(c)) - ) - .map((comment) => { + ?.map((comment) => { if (comment.value.includes("@noflow")) { return { ...comment, @@ -39,8 +65,23 @@ const removeComments = ( }; } - return comment; - }) || rootNode.comments; + return { + ...comment, + value: comment.value + .split("\n") + .map((line) => stripOutFlowESLintRuleFromNonFlowRules(line)) + .filter((line) => !flowComments.some((c) => line.includes(c))) + .join("\n"), + }; + }) + ?.map((comment) => ({ + ...comment, + value: stripOutFlowESLintRuleFromNonFlowRules(comment.value), + })) + ?.filter( + (comment) => !flowComments.some((c) => comment.value.includes(c)) + ) + ?.filter((comment) => comment.value.trim()) || rootNode.comments; } }; @@ -49,11 +90,24 @@ const removeComments = ( */ export function removeFlowComments({ file }: TransformerInput) { traverse(file, { - Program(path) { - removeComments(path); + enter({ node }) { + // @ts-expect-error comments doesn't exist + if (node.comments) { + // @ts-expect-error comments doesn't exist + node.comments = node.comments + // @ts-expect-error comments doesn't exist + ?.map((comment) => ({ + ...comment, + value: stripOutFlowESLintRuleFromNonFlowRules(comment.value), + })) + ?.filter( + // @ts-expect-error comments doesn't exist + (comment) => !flowComments.some((c) => comment.value.includes(c)) + ); + } }, - BlockStatement(path) { - removeComments(path); + Program(path) { + removeTopLevelComments(path); }, }); } diff --git a/src/convert/transform-runners.ts b/src/convert/transform-runners.ts index 979809d..8d098c0 100644 --- a/src/convert/transform-runners.ts +++ b/src/convert/transform-runners.ts @@ -1,5 +1,5 @@ import { hasJSX } from "./utils/common"; -import { addImports } from "./add-imports"; +import { moveImports } from "./move-imports"; import { addWatermark } from "./add-watermark"; import { transformJSX } from "./jsx"; import { transformDeclarations } from "./declarations"; @@ -46,7 +46,7 @@ export const patternTransformRunner: Transformer = standardTransformRunnerFactory(transformPatterns); export const importTransformRunner: Transformer = - standardTransformRunnerFactory(addImports); + standardTransformRunnerFactory(moveImports); export const watermarkTransformRunner: Transformer = standardTransformRunnerFactory(addWatermark); diff --git a/src/convert/type-annotations.test.ts b/src/convert/type-annotations.test.ts index c99daab..c2df3d9 100644 --- a/src/convert/type-annotations.test.ts +++ b/src/convert/type-annotations.test.ts @@ -72,6 +72,14 @@ describe("transform type annotations", () => { expect(await transform(src)).toBe(expected); }); + it("Converts maybe function", async () => { + const src = dedent` + const a: ?() => void = null;`; + const expected = dedent` + const a: (() => void) | null | undefined = null;`; + expect(await transform(src)).toBe(expected); + }); + it("Keeps void promises as void", async () => { const src = `function f(): Promise {};`; const expected = `function f(): Promise {};`; @@ -158,7 +166,7 @@ describe("transform type annotations", () => { it("Does not convert string Object key types to records", async () => { const src = `const MyObj: {[key: string]: ValueType} = {};`; const expected = dedent`const MyObj: { - [key: string]: ValueType + [key: string]: ValueType; } = {};`; expect(await transform(src)).toBe(expected); }); @@ -166,7 +174,7 @@ describe("transform type annotations", () => { it("Does not convert number Object key types to records", async () => { const src = `const MyObj: {[key: number]: ValueType} = {};`; const expected = dedent`const MyObj: { - [key: number]: ValueType + [key: number]: ValueType; } = {};`; expect(await transform(src)).toBe(expected); }); @@ -323,7 +331,7 @@ describe("transform type annotations", () => { `; const expected = dedent` type MockApiOptions = { - errors: jest.MockedFunction + errors: jest.MockedFunction; }; `; expect(await transform(src)).toBe(expected); @@ -342,7 +350,7 @@ describe("transform type annotations", () => { startDate: moment };`; const expected = dedent`type Test = { - startDate: moment.Moment + startDate: moment.Moment; };`; expect(await transform(src)).toBe(expected); }); @@ -367,9 +375,9 @@ describe("transform type annotations", () => { // React - it("Converts React.Node to React.ReactElement in function return", async () => { + it("Converts React.Node to ReactNode in function return", async () => { const src = `const Component = (props: Props): React.Node => {return
};`; - const expected = `const Component = (props: Props): React.ReactElement => {return
};`; + const expected = `const Component = (props: Props) => {return
};`; expect(await transform(src)).toBe(expected); }); @@ -441,9 +449,9 @@ describe("transform type annotations", () => { expect(await transform(src)).toBe(expected); }); - it("Converts React.MixedElement", async () => { + it("Strips React.MixedElement", async () => { const src = `function f(): React.MixedElement {};`; - const expected = `function f(): React.ReactElement {};`; + const expected = `function f() {};`; expect(await transform(src)).toBe(expected); }); @@ -453,18 +461,18 @@ describe("transform type annotations", () => { expect(await transform(src)).toBe(expected); }); - it("Converts React.Node to React.ReactElement in arrow function", async () => { + it("Converts React.Node to ReactNode in arrow function", async () => { const src = `const Component = (props: Props): React.Node => {return
};`; - const expected = `const Component = (props: Props): React.ReactElement => {return
};`; + const expected = `const Component = (props: Props) => {return
};`; expect(await transform(src)).toBe(expected); }); - it("Converts React.Node to React.ReactElement or null in arrow function return", async () => { + it("Converts React.Node to ReactNode in arrow function return", async () => { const src = dedent`const Component = (props: Props): React.Node => { if (foo) return (
); return null; };`; - const expected = dedent`const Component = (props: Props): React.ReactElement | null => { + const expected = dedent`const Component = (props: Props) => { if (foo) return (
); return null; };`; @@ -473,16 +481,16 @@ describe("transform type annotations", () => { it("Converts React.Node to React.ReactElement in normal function", async () => { const src = `function Component(props: Props): React.Node {return
};`; - const expected = `function Component(props: Props): React.ReactElement {return
};`; + const expected = `function Component(props: Props) {return
};`; expect(await transform(src)).toBe(expected); }); - it("Converts React.Node to React.ReactElement or null in normal function return", async () => { + it("Strips React.Node in normal function return", async () => { const src = dedent`function Component(props: Props): React.Node { if (foo) return (
); return null; };`; - const expected = dedent`function Component(props: Props): React.ReactElement | null { + const expected = dedent`function Component(props: Props) { if (foo) return (
); return null; };`; @@ -507,21 +515,21 @@ describe("transform type annotations", () => { expect(await transform(src)).toBe(expected); }); - it("Converts React.ElementConfig to JSX.LibraryManagedAttributes", async () => { + it("Converts React.ElementConfig to ComponentProps", async () => { const src = `type Test = React.ElementConfig;`; - const expected = `type Test = JSX.LibraryManagedAttributes>;`; + const expected = `type Test = ComponentProps;`; expect(await transform(src)).toBe(expected); }); it("Converts React.ElementConfig and keeps typeof", async () => { const src = `type Test = React.ElementConfig;`; - const expected = `type Test = JSX.LibraryManagedAttributes>;`; + const expected = `type Test = ComponentProps;`; expect(await transform(src)).toBe(expected); }); it("Converts React.ElementConfig with indexing", async () => { const src = `type Test = $PropertyType, 'foo'>;`; - const expected = `type Test = JSX.LibraryManagedAttributes>['foo'];`; + const expected = `type Test = ComponentProps['foo'];`; expect(await transform(src)).toBe(expected); }); @@ -532,7 +540,7 @@ describe("transform type annotations", () => { };`; const expected = dedent` type Props = { - children: Array | MenuChildren + children: Array | MenuChildren; };`; expect(await transform(src)).toBe(expected); }); @@ -595,14 +603,14 @@ describe("transform type annotations", () => { it("Does not convert {} to Record if an object has any properties", async () => { // dedent messes up the indentation of the string const src = `function f(): { - prop: boolean + prop: boolean; } {return {}} let af: () => { - prop: boolean + prop: boolean; } class C { m(): { - prop: boolean + prop: boolean; } {return {}} }`; @@ -764,9 +772,9 @@ class C { const expected = dedent` export type Test = { - (arg1: undefined | Example | Example[], arg2?: () => T | null | undefined): T, - (arg1: undefined | Example | Example[]): Attributes, - foo: number + (arg1: undefined | Example | Example[], arg2?: (() => T) | null | undefined): T; + (arg1: undefined | Example | Example[]): Attributes; + foo: number; }; `; expect(await transform(src)).toBe(expected); @@ -783,9 +791,9 @@ class C { const expected = dedent` type Test = { - (arg1: undefined | Example | Example[], arg2?: () => T | null | undefined): T, - (arg1: undefined | Example | Example[]): Attributes, - foo: number + (arg1: undefined | Example | Example[], arg2?: (() => T) | null | undefined): T; + (arg1: undefined | Example | Example[]): Attributes; + foo: number; }; `; expect(await transform(src)).toBe(expected); diff --git a/src/convert/type-annotations.ts b/src/convert/type-annotations.ts index 66377df..e925580 100644 --- a/src/convert/type-annotations.ts +++ b/src/convert/type-annotations.ts @@ -1,7 +1,7 @@ import * as t from "@babel/types"; import traverse from "@babel/traverse"; -import { replaceWith, inheritLocAndComments } from "./utils/common"; -import { migrateType } from "./migrate/type"; +import { remove, replaceWith, inheritLocAndComments } from "./utils/common"; +import { migrateType, REMOVE_ME_ID } from "./migrate/type"; import { migrateTypeParameterDeclaration } from "./migrate/type-parameter"; import { TransformerInput } from "./transformer"; import { MetaData } from "./migrate/metadata"; @@ -60,14 +60,28 @@ export function transformTypeAnnotations({ metaData.returnType = true; } - replaceWith( - path, - t.tsTypeAnnotation( - migrateType(reporter, state, path.node.typeAnnotation, metaData) - ), - state.config.filePath, - reporter + const tsType = migrateType( + reporter, + state, + path.node.typeAnnotation, + metaData ); + + if ( + metaData.returnType && + tsType.type === "TSTypeReference" && + tsType.typeName.type === "Identifier" && + tsType.typeName.name === REMOVE_ME_ID + ) { + remove(path, state.config.filePath, reporter); + } else { + replaceWith( + path, + t.tsTypeAnnotation(tsType), + state.config.filePath, + reporter + ); + } }, TypeParameterDeclaration(path) { diff --git a/src/convert/utils/common.ts b/src/convert/utils/common.ts index c8710d0..4f217b6 100644 --- a/src/convert/utils/common.ts +++ b/src/convert/utils/common.ts @@ -5,6 +5,8 @@ import { TransformerInput } from "../transformer"; import MigrationReporter from "../../runner/migration-reporter"; import { logger } from "../../runner/logger"; +const FORCE_JSX_COMMENT = "@force-jsx"; + /** * Determine whether the file contains any JSX * @param file : File source to check @@ -12,6 +14,18 @@ import { logger } from "../../runner/logger"; export function hasJSX({ file }: TransformerInput): boolean { let found = false; traverse(file, { + Program(path: NodePath) { + if ( + path.node.body?.some((node) => + // @ts-expect-error comments doesn't exist + node.comments?.some((comment) => + comment.value.includes(FORCE_JSX_COMMENT) + ) + ) + ) { + found = true; + } + }, JSXElement() { found = true; }, @@ -191,6 +205,19 @@ export function addEmptyLineInProgramPath(path: NodePath) { path.unshiftContainer("body", t.noop()); } +export function remove( + path: NodePath, + filePath: string, + reporter: MigrationReporter +) { + try { + path.remove(); + } catch (e) { + // Catch the error so conversion of the file can continue. + reporter.error(filePath, e); + } +} + /** * Recast uses a different format for comments. We need to manually copy them over to the new node. * We also attach the old location so that Recast prints it at the same place. diff --git a/src/convert/utils/matchers.ts b/src/convert/utils/matchers.ts index ba316fc..6175644 100644 --- a/src/convert/utils/matchers.ts +++ b/src/convert/utils/matchers.ts @@ -9,7 +9,7 @@ export function isIdentifierNamed(name: string) { * @param leftName - Left side of the type, e.g. React * @param rightName - Right side of the type */ -export function matchesFullyQualifiedName(leftName: string, rightName: string) { +function matchesFullyQualifiedName(leftName: string, rightName: string) { const leftMatcher = isIdentifierNamed(leftName); const rightMatcher = isIdentifierNamed(rightName); return (node: t.Identifier | t.TSQualifiedName) => @@ -17,3 +17,12 @@ export function matchesFullyQualifiedName(leftName: string, rightName: string) { leftMatcher(node.left) && rightMatcher(node.right); } + +/** + * Utility for checking React.* (e.g. matches both React.Node and Node) + */ +export function matchesReact(specifierName: string) { + return (node: t.Identifier | t.TSQualifiedName) => + isIdentifierNamed(specifierName)(node) || + matchesFullyQualifiedName("React", specifierName)(node); +} diff --git a/src/convert/utils/type-mappings.ts b/src/convert/utils/type-mappings.ts index 13ab56d..4b1af5b 100644 --- a/src/convert/utils/type-mappings.ts +++ b/src/convert/utils/type-mappings.ts @@ -3,6 +3,8 @@ export const ReactTypes = { Child: "ReactChild", Children: "ReactChildren", Text: "ReactText", + Element: "ReactElement", + ElementConfig: "ComponentProps", Fragment: "ReactFragment", FragmentType: "ComponentType", Portal: "ReactPortal", diff --git a/src/test/regression/__snapshots__/regression.test.ts.snap b/src/test/regression/__snapshots__/regression.test.ts.snap index 815cf6f..580b7ba 100644 --- a/src/test/regression/__snapshots__/regression.test.ts.snap +++ b/src/test/regression/__snapshots__/regression.test.ts.snap @@ -6,7 +6,7 @@ exports[`Regression tests flow_typescript_differences 1`] = ` // bounded polymorphism function fooGood(obj: T): T { console.log(Math.abs(obj.x)); return obj; @@ -23,12 +23,12 @@ function fnOptional(x?: number) {} // Exact/partial types type ExactUser = { - name: string, - age: number + name: string; + age: number; }; type User = { - name: string, - age: number + name: string; + age: number; }; type OptionalUser = Partial; @@ -77,7 +77,7 @@ const onePlusArrow = [1, 2, 3].map((a) => a + 1); // Lookup types type A = { - thing: string + thing: string; }; // when the property is a string constant use $PropertyType (i.e. you know it when typing) @@ -110,15 +110,15 @@ declare var e: E; // E is number // Mapped types type InputType = { - hello: string + hello: string; }; type MappedType = Flow.ObjMap number>; type FormFieldDef = { - label: string, - description?: string, - default: T | (() => T) + label: string; + description?: string; + default: T | (() => T); }; type ExtractShape = (arg1: T) => FormFieldDef; @@ -137,34 +137,34 @@ declare class Adder { // Readonly type B = { - readonly b: string + readonly b: string; }; let b: B = {b: 'something'}; // Difference type C = Flow.Diff<{ - a: string, - b: number + a: string; + b: number; }, { - a: string + a: string; }>; // Rest type Props = { - name: string, - age: number + name: string; + age: number; }; const propsRest: Props = {name: 'Jon', age: 42}; const {age, ...otherProps} = propsRest; (otherProps as Partial>); // Same type F = { - (): string + (): string; }; const f: F = () => 'hello'; const hello: string = f(); diff --git a/src/test/test-files/force-jsx.js b/src/test/test-files/force-jsx.js new file mode 100644 index 0000000..a38bea2 --- /dev/null +++ b/src/test/test-files/force-jsx.js @@ -0,0 +1,6 @@ +// @force-jsx + +function helloWorld() { + print('hello_world'); +} +helloWorld(); diff --git a/yarn.lock b/yarn.lock index 4b5f0e8..d79c16f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1116,15 +1116,26 @@ array.prototype.flat@^1.2.5: define-properties "^1.1.3" es-abstract "^1.19.0" +assert@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" + integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== + dependencies: + call-bind "^1.0.2" + is-nan "^1.3.2" + object-is "^1.1.5" + object.assign "^4.1.4" + util "^0.12.5" + assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw== -ast-types@0.14.2: - version "0.14.2" - resolved "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz#600b882df8583e3cd4f2df5fa20fa83759d4bdfd" - integrity sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA== +ast-types@^0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2" + integrity sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg== dependencies: tslib "^2.0.1" @@ -1143,6 +1154,13 @@ atob@^2.1.2: resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + babel-jest@^26.6.3: version "26.6.3" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" @@ -1311,6 +1329,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.5, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1591,6 +1620,15 @@ deepmerge@^4.2.2: resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -1598,6 +1636,15 @@ define-properties@^1.1.3: dependencies: object-keys "^1.0.12" +define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + define-property@^0.2.5: version "0.2.5" resolved "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" @@ -1730,6 +1777,18 @@ es-abstract@^1.19.0, es-abstract@^1.19.1: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.1" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -2291,6 +2350,13 @@ flow@^0.2.3: resolved "https://registry.npmjs.org/flow/-/flow-0.2.3.tgz#f8da65efa249127ec99376a28896572a9795d1af" integrity sha512-CFNnRuX6nsOmEjGvE5RP0zwGoebdU9k+IJJy97HHkc0Xdlyh5EFAmdOznTKhEQkWKWXTJp63bW0TWQZ+sywWRw== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -2345,6 +2411,11 @@ function-bind@^1.1.1: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" @@ -2369,6 +2440,17 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -2444,6 +2526,13 @@ globby@^11.0.4: merge2 "^1.3.0" slash "^3.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@^4.1.11, graceful-fs@^4.1.2: version "4.2.8" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" @@ -2474,11 +2563,28 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -2486,6 +2592,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has-value@^0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -2524,6 +2637,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -2614,7 +2734,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@^2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2642,6 +2762,14 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -2667,6 +2795,11 @@ is-buffer@^1.1.5: resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-callable@^1.1.3: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" @@ -2764,6 +2897,13 @@ is-generator-fn@^2.0.0: resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -2771,6 +2911,14 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: dependencies: is-extglob "^2.1.1" +is-nan@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" + integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + is-negative-zero@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" @@ -2844,6 +2992,13 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-typed-array@^1.1.3: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -3774,6 +3929,14 @@ object-inspect@^1.11.0, object-inspect@^1.9.0: resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -3796,6 +3959,16 @@ object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" +object.assign@^4.1.4: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + object.pick@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" @@ -4049,6 +4222,11 @@ posix-character-classes@^0.1.0: resolved "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -4151,12 +4329,13 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -recast@^0.20.4: - version "0.20.4" - resolved "https://registry.npmjs.org/recast/-/recast-0.20.4.tgz#db55983eac70c46b3fff96c8e467d65ffb4a7abc" - integrity sha512-6qLIBGGRcwjrTZGIiBpJVC/NeuXpogXNyRQpqU1zWPUigCphvApoCs9KIwDYh1eDuJ6dAFlQoi/QUyE5KQ6RBQ== +recast@0.23.4: + version "0.23.4" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.23.4.tgz#ca1bac7bfd3011ea5a28dfecb5df678559fb1ddf" + integrity sha512-qtEDqIZGVcSZCHniWwZWbRy79Dc6Wp3kT/UmDA2RJKBPg7+7k51aQBZirHmUGn5uvHf2rg8DkjizrN26k61ATw== dependencies: - ast-types "0.14.2" + assert "^2.0.0" + ast-types "^0.16.1" esprima "~4.0.0" source-map "~0.6.1" tslib "^2.0.1" @@ -4331,6 +4510,18 @@ set-blocking@^2.0.0: resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -4910,6 +5101,17 @@ use@^3.1.0: resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +util@^0.12.5: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -5005,6 +5207,17 @@ which-module@^2.0.0: resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== +which-typed-array@^1.1.14, which-typed-array@^1.1.2: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + which@^1.2.9: version "1.3.1" resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"