diff --git a/package.json b/package.json
index 12053b0..8971103 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,8 @@
"jscodeshift": "jscodeshift"
},
"dependencies": {
+ "@codemod.com/codemod-utils": "^1.0.0",
+ "@types/jscodeshift": "^0.12.0",
"chalk": "^2.4.2",
"eslint": "^6.6.0",
"execa": "^3.2.0",
@@ -22,7 +24,10 @@
},
"jest": {
"globals": {
- "baseDir": "../"
+ "baseDir": "../",
+ "ts-jest": {
+ "tsconfig": "./tsconfig.codemod.json"
+ }
},
"testEnvironment": "node",
"roots": [
@@ -32,13 +37,20 @@
"transform": {
"^.+\\.jsx?$": "babel-jest",
"^.+\\.tsx?$": "ts-jest"
- }
+ },
+ "transformIgnorePatterns": [
+ "/node_modules/(?!@codemod\\.com/codemod-utils)/.+\\.js$"
+ ],
+ "moduleFileExtensions": ["js", "ts", "tsx", "jsx"],
+ "extensionsToTreatAsEsm": [".ts", ".tsx", ".js", ".jsx"]
},
"devDependencies": {
"@babel/core": "^7.6.4",
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
"@babel/preset-env": "^7.6.3",
+ "@jest/globals": "^29.7.0",
"@types/jest": "^24.9.0",
+ "@types/node": "^22.12.0",
"@typescript-eslint/parser": "^7.8.0",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
diff --git a/transforms/__tests__/prop-types-typescript.codemod.test.ts b/transforms/__tests__/prop-types-typescript.codemod.test.ts
new file mode 100644
index 0000000..ee5331b
--- /dev/null
+++ b/transforms/__tests__/prop-types-typescript.codemod.test.ts
@@ -0,0 +1,540 @@
+/* @license
+ISC License
+Copyright (c) 2023, Mark Skelton
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+/*
+Changes to the original file: changed tests structure
+*/
+
+import * as assert from "assert";
+import transform from "../prop-types-typescript.codemod";
+import * as jscodeshift from "jscodeshift";
+import type { API } from "jscodeshift";
+
+const buildApi = (parser: string | undefined): API => ({
+ j: parser ? jscodeshift.withParser(parser) : jscodeshift,
+ jscodeshift: parser ? jscodeshift.withParser(parser) : jscodeshift,
+ stats: () => {
+ console.error(
+ "The stats function was called, which is not supported on purpose"
+ );
+ },
+ report: () => {
+ console.error(
+ "The report function was called, which is not supported on purpose"
+ );
+ },
+});
+
+describe("ratchet", () => {
+ it("arrow-function", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport const MyComponent = (props) => {\n return \n}\n\nMyComponent.propTypes = {\n bar: PropTypes.string.isRequired,\n foo: PropTypes.number,\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n bar: string\n foo?: number\n}\n\nexport const MyComponent = (props: MyComponentProps) => {\n return \n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("class-component-static", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport class MyComponent extends React.Component {\n static propTypes = {\n bar: PropTypes.string.isRequired,\n foo: PropTypes.number,\n }\n\n render() {\n return \n }\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n bar: string\n foo?: number\n}\n\nexport class MyComponent extends React.Component {\n render() {\n return \n }\n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("class-component", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport class MyComponent extends React.Component {\n render() {\n return \n }\n}\n\nMyComponent.propTypes = {\n bar: PropTypes.string.isRequired,\n foo: PropTypes.number,\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n bar: string\n foo?: number\n}\n\nexport class MyComponent extends React.Component {\n render() {\n return \n }\n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("comments", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function MyComponent(props) {\n return \n}\n\nMyComponent.propTypes = {\n /**\n * A string with a\n * wrapping comment.\n * @example "foo"\n */\n bar: PropTypes.string.isRequired,\n /**\n * Some function\n */\n foo: PropTypes.func,\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n /**\n * A string with a\n * wrapping comment.\n * @example "foo"\n */\n bar: string\n /**\n * Some function\n */\n foo?(...args: unknown[]): unknown\n}\n\nexport function MyComponent(props: MyComponentProps) {\n return \n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("complex-props", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function MyComponent(props) {\n return \n}\n\nMyComponent.propTypes = {\n optionalArray: PropTypes.array,\n optionalBool: PropTypes.bool,\n optionalFunc: PropTypes.func,\n optionalNumber: PropTypes.number,\n optionalObject: PropTypes.object,\n optionalString: PropTypes.string,\n optionalSymbol: PropTypes.symbol,\n optionalNode: PropTypes.node,\n optionalElement: PropTypes.element,\n optionalElementType: PropTypes.elementType,\n optionalEnum: PropTypes.oneOf(["News", "Photos"]),\n optionalNumericEnum: PropTypes.oneOf([1, 2, 3]),\n optionalMixedEnum: PropTypes.oneOf([1, "Unknown", false, () => {}]),\n optionalUnknownEnum: PropTypes.oneOf(Object.keys(arr)),\n optionalUnion: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),\n optionalArrayOf: PropTypes.arrayOf(PropTypes.number),\n optionalObjectOf: PropTypes.objectOf(PropTypes.number),\n optionalInstanceOf: PropTypes.instanceOf(Message),\n optionalObjectWithShape: PropTypes.shape({\n optionalProperty: PropTypes.string,\n requiredProperty: PropTypes.number.isRequired,\n functionProperty: PropTypes.func,\n }),\n optionalObjectWithStrictShape: PropTypes.exact({\n optionalProperty: PropTypes.string,\n requiredProperty: PropTypes.number.isRequired,\n }),\n requiredArray: PropTypes.array.isRequired,\n requiredBool: PropTypes.bool.isRequired,\n requiredFunc: PropTypes.func.isRequired,\n requiredNumber: PropTypes.number.isRequired,\n requiredObject: PropTypes.object.isRequired,\n requiredString: PropTypes.string.isRequired,\n requiredSymbol: PropTypes.symbol.isRequired,\n requiredNode: PropTypes.node.isRequired,\n requiredElement: PropTypes.element.isRequired,\n requiredElementType: PropTypes.elementType.isRequired,\n requiredEnum: PropTypes.oneOf(["News", "Photos"]).isRequired,\n requiredUnion: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,\n requiredArrayOf: PropTypes.arrayOf(PropTypes.number).isRequired,\n requiredObjectOf: PropTypes.objectOf(PropTypes.number).isRequired,\n requiredInstanceOf: PropTypes.instanceOf(Message).isRequired,\n requiredObjectWithShape: PropTypes.shape({\n optionalProperty: PropTypes.string,\n requiredProperty: PropTypes.number.isRequired,\n }).isRequired,\n requiredObjectWithStrictShape: PropTypes.exact({\n optionalProperty: PropTypes.string,\n requiredProperty: PropTypes.number.isRequired,\n }).isRequired,\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n optionalArray?: unknown[]\n optionalBool?: boolean\n optionalFunc?(...args: unknown[]): unknown\n optionalNumber?: number\n optionalObject?: object\n optionalString?: string\n optionalSymbol?: symbol\n optionalNode?: React.ReactNode\n optionalElement?: React.ReactElement\n optionalElementType?: React.ElementType\n optionalEnum?: "News" | "Photos"\n optionalNumericEnum?: 1 | 2 | 3\n optionalMixedEnum?: 1 | "Unknown" | false | unknown\n optionalUnknownEnum?: unknown[]\n optionalUnion?: string | number\n optionalArrayOf?: number[]\n optionalObjectOf?: Record\n optionalInstanceOf?: Message\n optionalObjectWithShape?: {\n optionalProperty?: string\n requiredProperty: number\n functionProperty?(...args: unknown[]): unknown\n }\n optionalObjectWithStrictShape?: {\n optionalProperty?: string\n requiredProperty: number\n }\n requiredArray: unknown[]\n requiredBool: boolean\n requiredFunc(...args: unknown[]): unknown\n requiredNumber: number\n requiredObject: object\n requiredString: string\n requiredSymbol: symbol\n requiredNode: React.ReactNode\n requiredElement: React.ReactElement\n requiredElementType: React.ElementType\n requiredEnum: "News" | "Photos"\n requiredUnion: string | number\n requiredArrayOf: number[]\n requiredObjectOf: Record\n requiredInstanceOf: Message\n requiredObjectWithShape: {\n optionalProperty?: string\n requiredProperty: number\n }\n requiredObjectWithStrictShape: {\n optionalProperty?: string\n requiredProperty: number\n }\n}\n\nexport function MyComponent(props: MyComponentProps) {\n return \n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {
+ "preserve-prop-types": "unconverted",
+ });
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("custom-validator", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function MyComponent(props) {\n return \n}\n\nMyComponent.propTypes = {\n a: PropTypes.string,\n b: function () {},\n c: () => {},\n d: PropTypes.arrayOf(function() {}),\n e: PropTypes.arrayOf(() => {}),\n f: PropTypes.objectOf(function() {}),\n g: PropTypes.objectOf(() => {}),\n h: PropTypes.arrayOf(function() {}).isRequired,\n i: PropTypes.arrayOf(() => {}).isRequired,\n j: PropTypes.objectOf(function() {}).isRequired,\n k: PropTypes.objectOf(() => {}).isRequired\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n a?: string\n b?: unknown\n c?: unknown\n d?: unknown\n e?: unknown\n f?: unknown\n g?: unknown\n h: unknown\n i: unknown\n j: unknown\n k: unknown\n}\n\nexport function MyComponent(props: MyComponentProps) {\n return \n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("extended-props", () => {
+ const INPUT =
+ 'import BaseComponent from "./base"\nimport React from "react"\n\nexport function MyComponent(props) {\n return \n}\n\nMyComponent.propTypes = BaseComponent.propTypes\n';
+
+ const OUTPUT =
+ 'import BaseComponent from "./base"\nimport React from "react"\n\nexport function MyComponent(props) {\n return \n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("forward-ref-and-func", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React, { forwardRef } from "react"\n\nexport const MyComponent = forwardRef((props, ref) => {\n return \n})\n\nMyComponent.propTypes = {\n bar: PropTypes.string.isRequired,\n foo: PropTypes.number,\n}\n\nexport function ComponentA(props) {\n return \n}\n\nComponentA.propTypes = {\n a: PropTypes.string.isRequired,\n b: PropTypes.number,\n}\n';
+
+ const OUTPUT =
+ 'import React, { forwardRef } from "react"\n\ninterface MyComponentProps {\n bar: string\n foo?: number\n}\n\nexport const MyComponent = forwardRef((props, ref) => {\n return \n})\n\ninterface ComponentAProps {\n a: string\n b?: number\n}\n\nexport function ComponentA(props: ComponentAProps) {\n return \n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("forward-ref", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nconst MyComponent = React.forwardRef((props, ref) => {\n return \n})\n\nMyComponent.propTypes = {\n bar: PropTypes.string.isRequired,\n foo: PropTypes.number,\n}\n\nexport default MyComponent\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n bar: string\n foo?: number\n}\n\nconst MyComponent = React.forwardRef((props, ref) => {\n return \n})\n\nexport default MyComponent\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("function-and-class", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function ComponentA(props) {\n return \n}\n\nComponentA.propTypes = {\n a: PropTypes.string.isRequired,\n b: PropTypes.number,\n}\n\nexport class ComponentB extends React.Component {\n render() {\n return \n }\n}\n\nComponentB.propTypes = {\n c: PropTypes.array,\n d: PropTypes.object.isRequired,\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface ComponentAProps {\n a: string\n b?: number\n}\n\nexport function ComponentA(props: ComponentAProps) {\n return \n}\n\ninterface ComponentBProps {\n c?: unknown[]\n d: object\n}\n\nexport class ComponentB extends React.Component {\n render() {\n return \n }\n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("function-component", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function MyComponent(props) {\n return \n}\n\nMyComponent.propTypes = {\n bar: PropTypes.string.isRequired,\n foo: PropTypes.number,\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n bar: string\n foo?: number\n}\n\nexport function MyComponent(props: MyComponentProps) {\n return \n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("literal-prop", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function MyComponent(props) {\n return \n}\n\nMyComponent.propTypes = {\n \'data-testid\': PropTypes.string,\n}\n';
+
+ const OUTPUT =
+ "import React from \"react\"\n\ninterface MyComponentProps {\n 'data-testid'?: string\n}\n\nexport function MyComponent(props: MyComponentProps) {\n return \n}\n";
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("memo-export", () => {
+ const INPUT =
+ "import PropTypes from 'prop-types'\nimport React from 'react'\n\nexport const MyComponent = React.memo(function MyComponent(props) {\n return null\n})\n\nMyComponent.propTypes = {\n a: PropTypes.number\n}\n";
+
+ const OUTPUT =
+ "import React from 'react'\n\ninterface MyComponentProps {\n a?: number\n}\n\nexport const MyComponent = React.memo(function MyComponent(props: MyComponentProps) {\n return null\n})\n";
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("memo", () => {
+ const INPUT =
+ "import PropTypes from 'prop-types'\nimport React from 'react'\n\nconst MyComponent = React.memo(function MyComponent(props) {\n return null\n})\n\nMyComponent.propTypes = {\n a: PropTypes.number\n}\n";
+
+ const OUTPUT =
+ "import React from 'react'\n\ninterface MyComponentProps {\n a?: number\n}\n\nconst MyComponent = React.memo(function MyComponent(props: MyComponentProps) {\n return null\n})\n";
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("multiple-class-components-static", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport class ComponentA extends React.Component {\n static propTypes = {\n a: PropTypes.string.isRequired,\n b: PropTypes.number,\n }\n\n render() {\n return \n }\n}\n\nexport class ComponentB extends React.Component {\n static propTypes = {\n c: PropTypes.array,\n d: PropTypes.object.isRequired,\n }\n\n render() {\n return \n }\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface ComponentAProps {\n a: string\n b?: number\n}\n\nexport class ComponentA extends React.Component {\n render() {\n return \n }\n}\n\ninterface ComponentBProps {\n c?: unknown[]\n d: object\n}\n\nexport class ComponentB extends React.Component {\n render() {\n return \n }\n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("multiple-components", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function ComponentA(props) {\n return \n}\n\nComponentA.propTypes = {\n a: PropTypes.string.isRequired,\n b: PropTypes.number,\n}\n\nexport function ComponentB(props) {\n return \n}\n\nComponentB.propTypes = {\n c: PropTypes.array,\n d: PropTypes.object.isRequired,\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface ComponentAProps {\n a: string\n b?: number\n}\n\nexport function ComponentA(props: ComponentAProps) {\n return \n}\n\ninterface ComponentBProps {\n c?: unknown[]\n d: object\n}\n\nexport function ComponentB(props: ComponentBProps) {\n return \n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("no-export", () => {
+ const INPUT =
+ "import PropTypes from 'prop-types'\nimport React from 'react'\n\nfunction MyComponent(props) {\n return null\n}\n\nMyComponent.propTypes = {\n a: PropTypes.number\n}\n";
+
+ const OUTPUT =
+ "import React from 'react'\n\ninterface MyComponentProps {\n a?: number\n}\n\nfunction MyComponent(props: MyComponentProps) {\n return null\n}\n";
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("no-prop-types", () => {
+ const INPUT =
+ 'import React from "react"\n\nexport function MyComponent(props) {\n return \n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(actualOutput, undefined);
+ });
+
+ it("odd-required", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport const MyComponent = (props) => {\n return \n}\n\nMyComponent.propTypes = {\n a: PropTypes.arrayOf(PropTypes.shape({\n name: PropTypes.number.isRequired\n }).isRequired),\n b: PropTypes.objectOf(PropTypes.number.isRequired)\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n a?: {\n name: number\n }[]\n b?: Record\n}\n\nexport const MyComponent = (props: MyComponentProps) => {\n return \n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("preserve-none", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function ComponentA(props) {\n return \n}\n\nComponentA.propTypes = {\n ...OtherComponent,\n a: PropTypes.string.isRequired,\n b() {}\n}\n\nexport function ComponentB(props) {\n return \n}\n\nComponentB.propTypes = {\n ...ThisComponent,\n c: PropTypes.number,\n d() {}\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface ComponentAProps {\n a: string\n b?: unknown\n}\n\nexport function ComponentA(props: ComponentAProps) {\n return \n}\n\ninterface ComponentBProps {\n c?: number\n d?: unknown\n}\n\nexport function ComponentB(props: ComponentBProps) {\n return \n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("preserve-prop-types", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function MyComponent(props) {\n return \n}\n\nMyComponent.propTypes = {\n bar: PropTypes.string.isRequired,\n foo: PropTypes.number,\n}\n';
+
+ const OUTPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\ninterface MyComponentProps {\n bar: string\n foo?: number\n}\n\nexport function MyComponent(props: MyComponentProps) {\n return \n}\n\nMyComponent.propTypes = {\n bar: PropTypes.string.isRequired,\n foo: PropTypes.number,\n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {
+ "preserve-prop-types": "all",
+ });
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("preserve-unconverted-shape", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function MyComponent(props) {\n return \n}\n\nMyComponent.propTypes = {\n a: PropTypes.string,\n b: function () {},\n c: PropTypes.shape({\n d: PropTypes.bool\n })\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n a?: string\n b?: unknown\n c?: {\n d?: boolean\n }\n}\n\nexport function MyComponent(props: MyComponentProps) {\n return \n}\n\nMyComponent.propTypes = {\n b: function () {}\n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {
+ "preserve-prop-types": "unconverted",
+ });
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("preserve-unconverted-static", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport class MyComponent extends React.Component {\n static propTypes = {\n bar: PropTypes.string.isRequired,\n foo() {}\n }\n\n render() {\n return \n }\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n bar: string\n foo?: unknown\n}\n\nexport class MyComponent extends React.Component {\n static propTypes = {\n foo() {}\n }\n\n render() {\n return \n }\n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {
+ "preserve-prop-types": "unconverted",
+ });
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("preserve-unconverted", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function MyComponent(props) {\n return \n}\n\nMyComponent.propTypes = {\n ...OtherComponent.propTypes,\n a: PropTypes.string,\n b: function () {},\n c: () => {},\n d: PropTypes.arrayOf(function() {}),\n e: PropTypes.arrayOf(() => {}),\n f: PropTypes.objectOf(function() {}),\n g: PropTypes.objectOf(() => {}),\n}\n';
+
+ const OUTPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\ninterface MyComponentProps {\n a?: string\n b?: unknown\n c?: unknown\n d?: unknown\n e?: unknown\n f?: unknown\n g?: unknown\n}\n\nexport function MyComponent(props: MyComponentProps) {\n return \n}\n\nMyComponent.propTypes = {\n ...OtherComponent.propTypes,\n b: function () {},\n c: () => {},\n d: PropTypes.arrayOf(function() {}),\n e: PropTypes.arrayOf(() => {}),\n f: PropTypes.objectOf(function() {}),\n g: PropTypes.objectOf(() => {})\n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {
+ "preserve-prop-types": "unconverted",
+ });
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("spread-element", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function MyComponent(props) {\n return \n}\n\nMyComponent.propTypes = {\n ...OtherComponent.propTypes,\n a: PropTypes.string,\n}\n';
+
+ const OUTPUT =
+ 'import React from "react"\n\ninterface MyComponentProps {\n a?: string\n}\n\nexport function MyComponent(props: MyComponentProps) {\n return \n}\n\nMyComponent.propTypes = {\n ...OtherComponent.propTypes\n}\n';
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {
+ "preserve-prop-types": "unconverted",
+ });
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+
+ it("typescript", () => {
+ const INPUT =
+ 'import PropTypes from "prop-types"\nimport React from "react"\n\nexport function MyComponent(props) {\n const foo: string = \'bar\'\n return \n}\n\nMyComponent.propTypes = {\n bar: PropTypes.string.isRequired,\n foo: PropTypes.number,\n}\n';
+
+ const OUTPUT =
+ "import React from \"react\"\n\ninterface MyComponentProps {\n bar: string\n foo?: number\n}\n\nexport function MyComponent(props: MyComponentProps) {\n const foo: string = 'bar'\n return \n}\n";
+
+ const fileInfo = {
+ path: "index.js",
+ source: INPUT,
+ };
+
+ const actualOutput = transform(fileInfo, buildApi("tsx"), {});
+ assert.deepEqual(
+ actualOutput?.replace(/\W/gm, ""),
+ OUTPUT.replace(/\W/gm, "")
+ );
+ });
+});
diff --git a/transforms/prop-types-typescript.codemod.ts b/transforms/prop-types-typescript.codemod.ts
new file mode 100644
index 0000000..f1e9e9c
--- /dev/null
+++ b/transforms/prop-types-typescript.codemod.ts
@@ -0,0 +1,464 @@
+/*! @license
+
+ISC License
+
+Copyright (c) 2023, Mark Skelton
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+/*
+Changes to the original file: fixed ts errors
+*/
+
+import type { NodePath } from "ast-types/lib/node-path.js";
+import type {
+ API,
+ Collection,
+ CommentBlock,
+ CommentLine,
+ FileInfo,
+ Identifier,
+ JSCodeshift,
+ Literal,
+ Options,
+ TSAnyKeyword,
+ TSFunctionType,
+} from "jscodeshift";
+
+let j: JSCodeshift;
+let options: {
+ preservePropTypes: "none" | "unconverted" | "all";
+};
+
+function reactType(type: string) {
+ return j.tsQualifiedName(j.identifier("React"), j.identifier(type));
+}
+
+type TSType = {
+ comments: (CommentLine | CommentBlock)[];
+ key: Identifier | Literal;
+ required: boolean;
+ type: TSAnyKeyword | TSFunctionType;
+};
+
+function createPropertySignature({ comments, key, required, type }: TSType) {
+ if (type.type === "TSFunctionType") {
+ return j.tsMethodSignature.from({
+ comments,
+ key,
+ optional: !required,
+ parameters: type.parameters,
+ typeAnnotation: type.typeAnnotation,
+ });
+ }
+
+ return j.tsPropertySignature.from({
+ comments,
+ key,
+ optional: !required,
+ typeAnnotation: j.tsTypeAnnotation(type),
+ });
+}
+
+function isCustomValidator(path: NodePath) {
+ return (
+ path.get("type").value === "FunctionExpression" ||
+ path.get("type").value === "ArrowFunctionExpression"
+ );
+}
+
+const resolveRequired = (path: NodePath) =>
+ isRequired(path) ? path.get("object") : path;
+
+function getTSType(path: NodePath): any {
+ const { value: name } =
+ path.get("type").value === "MemberExpression"
+ ? path.get("property", "name")
+ : path.get("callee", "property", "name");
+
+ switch (name) {
+ case "func": {
+ const restElement = j.restElement.from({
+ argument: j.identifier("args"),
+ typeAnnotation: j.tsTypeAnnotation(j.tsArrayType(j.tsUnknownKeyword())),
+ });
+
+ return j.tsFunctionType.from({
+ parameters: [restElement],
+ typeAnnotation: j.tsTypeAnnotation(j.tsUnknownKeyword()),
+ });
+ }
+
+ case "arrayOf": {
+ const type = path.get("arguments", 0);
+ return isCustomValidator(type)
+ ? j.tsUnknownKeyword()
+ : j.tsArrayType(getTSType(resolveRequired(type)));
+ }
+
+ case "objectOf": {
+ const type = path.get("arguments", 0);
+ return isCustomValidator(type)
+ ? j.tsUnknownKeyword()
+ : j.tsTypeReference(
+ j.identifier("Record"),
+ j.tsTypeParameterInstantiation([
+ j.tsStringKeyword(),
+ getTSType(resolveRequired(type)),
+ ])
+ );
+ }
+
+ case "oneOf": {
+ const arg = path.get("arguments", 0);
+
+ return arg.get("type").value !== "ArrayExpression"
+ ? j.tsArrayType(j.tsUnknownKeyword())
+ : j.tsUnionType(
+ arg
+ .get("elements")
+ .value.map(({ type, value }: { type: any; value: any }) => {
+ switch (type) {
+ case "StringLiteral":
+ return j.tsLiteralType(j.stringLiteral(value));
+
+ case "NumericLiteral":
+ return j.tsLiteralType(j.numericLiteral(value));
+
+ case "BooleanLiteral":
+ return j.tsLiteralType(j.booleanLiteral(value));
+
+ default:
+ return j.tsUnknownKeyword();
+ }
+ })
+ );
+ }
+
+ case "oneOfType":
+ return j.tsUnionType(path.get("arguments", 0, "elements").map(getTSType));
+
+ case "instanceOf":
+ return j.tsTypeReference(
+ j.identifier(path.get("arguments", 0, "name").value)
+ );
+
+ case "shape":
+ case "exact":
+ return j.tsTypeLiteral(
+ path
+ .get("arguments", 0, "properties")
+ .map(mapType)
+ .map(createPropertySignature)
+ );
+ }
+
+ const map = {
+ any: j.tsAnyKeyword(),
+ array: j.tsArrayType(j.tsUnknownKeyword()),
+ bool: j.tsBooleanKeyword(),
+ element: j.tsTypeReference(reactType("ReactElement")),
+ elementType: j.tsTypeReference(reactType("ElementType")),
+ node: j.tsTypeReference(reactType("ReactNode")),
+ number: j.tsNumberKeyword(),
+ object: j.tsObjectKeyword(),
+ string: j.tsStringKeyword(),
+ symbol: j.tsSymbolKeyword(),
+ } as const;
+
+ return name in map ? map[name as keyof typeof map] : j.tsUnknownKeyword();
+}
+
+const isRequired = (path: NodePath) =>
+ path.get("type").value === "MemberExpression" &&
+ path.get("property", "name").value === "isRequired";
+
+function mapType(path: NodePath): TSType {
+ const required = isRequired(path.get("value"));
+ const key = path.get("key").value;
+ const comments = path.get("leadingComments").value;
+ const type = getTSType(
+ required ? path.get("value", "object") : path.get("value")
+ );
+
+ // If all types should be removed or the type was able to be converted,
+ // we remove the type.
+ if (options.preservePropTypes !== "all" && type.type !== "TSUnknownKeyword") {
+ path.replace();
+ }
+
+ return {
+ comments: comments ?? [],
+ key,
+ required,
+ type,
+ };
+}
+
+type CollectedTypes = {
+ component: string;
+ types: TSType[];
+}[];
+
+function getTSTypes(
+ source: Collection,
+ getComponentName: (path: NodePath) => string
+) {
+ const collected = [] as CollectedTypes;
+ const propertyTypes = ["Property", "ObjectProperty", "ObjectMethod"];
+
+ source
+ .filter((path) => path.value)
+ .forEach((path) => {
+ collected.push({
+ component: getComponentName(path),
+ types: path
+ .filter(
+ ({ value }: { value: any }) => propertyTypes.includes(value.type),
+ null
+ )
+ .map(mapType, null),
+ });
+ });
+
+ return collected;
+}
+
+function getFunctionParent(path: NodePath): NodePath {
+ return path.parent.get("type").value === "Program"
+ ? path
+ : getFunctionParent(path.parent);
+}
+
+function getComponentName(path: NodePath) {
+ const root =
+ path.get("type").value === "ArrowFunctionExpression" ? path.parent : path;
+
+ return root.get("id", "name").value ?? root.parent.get("id", "name").value;
+}
+function createInterface(path: NodePath, componentTypes: CollectedTypes) {
+ const componentName = getComponentName(path);
+ const types = componentTypes.find((t) => t.component === componentName);
+ const typeName = `${componentName}Props`;
+
+ // If the component doesn't have propTypes, ignore it
+ if (!types) return;
+
+ // Add the TS types before the function/class
+ getFunctionParent(path).insertBefore(
+ j.tsInterfaceDeclaration(
+ j.identifier(typeName),
+ j.tsInterfaceBody(types.types.map(createPropertySignature))
+ )
+ );
+
+ return typeName;
+}
+/**
+ * If forwardRef is being used, declare the props.
+ * Otherwise, return false
+ */
+function addForwardRefTypes(path: NodePath, typeName: string): boolean {
+ // for `React.forwardRef()`
+ if (path.node.callee?.property?.name === "forwardRef") {
+ path.node.callee.property.name = `forwardRef`;
+ return true;
+ }
+ // if calling `forwardRef()` directly
+ if (path.node.callee?.name === "forwardRef") {
+ path.node.callee.name = `forwardRef`;
+ return true;
+ }
+ return false;
+}
+
+function addFunctionTSTypes(
+ source: Collection,
+ componentTypes: CollectedTypes
+) {
+ source.forEach((path) => {
+ const typeName = createInterface(path, componentTypes);
+ if (!typeName) return;
+
+ // add forwardRef types if present
+ if (addForwardRefTypes(path.parentPath, typeName)) return;
+ // Function components & Class Components
+ // Add the TS types to the props param
+ path.get("params", 0).value.typeAnnotation = j.tsTypeReference(
+ // For some reason, jscodeshift isn't adding the colon so we have to do
+ // that ourselves.
+ j.identifier(`: ${typeName}`)
+ );
+ });
+}
+
+function addClassTSTypes(source: Collection, componentTypes: CollectedTypes) {
+ source.find(j.ClassDeclaration).forEach((path) => {
+ const typeName = createInterface(path, componentTypes);
+ if (!typeName) return;
+
+ // Add the TS types to the React.Component super class
+ path.value.superTypeParameters = j.tsTypeParameterInstantiation([
+ j.tsTypeReference(j.identifier(typeName)),
+ ]);
+ });
+}
+
+function collectPropTypes(source: Collection) {
+ return source
+ .find(j.AssignmentExpression)
+ .filter(
+ (path) => path.get("left", "property", "name").value === "propTypes"
+ )
+ .map((path) => path.get("right", "properties"));
+}
+
+function collectStaticPropTypes(source: Collection) {
+ return source
+ .find(j.ClassProperty)
+ .filter((path) => !!path.value.static)
+ .filter((path) => path.get("key", "name").value === "propTypes")
+ .map((path) => path.get("value", "properties"));
+}
+
+function cleanup(
+ source: Collection,
+ propTypes: Collection,
+ staticPropTypes: Collection
+) {
+ propTypes.forEach((path) => {
+ if (!path.parent.get("right", "properties", "length").value) {
+ path.parent.prune();
+ }
+ });
+
+ staticPropTypes.forEach((path) => {
+ if (!path.parent.get("value", "properties", "length").value) {
+ path.parent.prune();
+ }
+ });
+
+ const propTypesUsages = source
+ .find(j.MemberExpression)
+ .filter((path) => path.get("object", "name").value === "PropTypes");
+
+ // We can remove the import without caring about the preserve-prop-types
+ // option since the criteria for removal is that no PropTypes.* member
+ // expressions exist.
+ if (propTypesUsages.length === 0) {
+ source
+ .find(j.ImportDeclaration)
+ .filter((path) => path.value.source.value === "prop-types")
+ .remove();
+ }
+}
+
+const isOnlyWhitespace = (str: string) => !/\S/.test(str);
+
+/**
+ * Guess the tab width of the file. This file is a modified version of recast's
+ * built-in tab width guessing with a modification to better handle files with
+ * block comments.
+ * @see https://github.com/benjamn/recast/blob/8cc1f42408c41b5616d82574f5552c2da3e11cf7/lib/lines.ts#L280-L314
+ */
+function guessTabWidth(source: string) {
+ const lines = source.split("\n");
+ const counts: number[] = [];
+ let lastIndent = 0;
+
+ for (const line of lines) {
+ // Whitespace-only lines don't tell us much about the likely tab width
+ if (isOnlyWhitespace(line)) {
+ continue;
+ }
+
+ // Calculate the indentation of the line excluding lines starting with an
+ // asterisk. This is because these lines are often part of block comments
+ // which are indented an extra space which throws off our tab width guessing.
+ const indent = line.match(/^(\s*)/)
+ ? line.trim().startsWith("*")
+ ? lastIndent
+ : RegExp.$1.length
+ : 0;
+
+ const diff = Math.abs(indent - lastIndent);
+ counts[diff] = ~~(counts[diff] ?? 0) + 1;
+ lastIndent = indent;
+ }
+
+ let maxCount = -1;
+ let result = 2;
+
+ // Loop through the counts array to find the most common tab width in the file
+ for (let tabWidth = 1; tabWidth < counts.length; tabWidth++) {
+ const count = counts[tabWidth];
+ if (count !== undefined && count > maxCount) {
+ maxCount = count;
+ result = tabWidth;
+ }
+ }
+
+ return result;
+}
+
+// Use the TSX to allow parsing of TypeScript code that still contains prop
+// types. Though not typical, this exists in the wild.
+export const parser = "tsx";
+
+export default function transform(file: FileInfo, api: API, opts: Options) {
+ j = api.jscodeshift;
+ const source = j(file.source);
+
+ // Parse the CLI options
+ options = {
+ preservePropTypes:
+ opts["preserve-prop-types"] === true
+ ? "all"
+ : opts["preserve-prop-types"] || "none",
+ };
+
+ const propTypes = collectPropTypes(source);
+
+ const tsTypes = getTSTypes(
+ propTypes,
+ (path) => path.parent.get("left", "object", "name").value
+ );
+
+ const staticPropTypes = collectStaticPropTypes(source);
+
+ if (propTypes.length === 0 && staticPropTypes.length === 0) {
+ return undefined;
+ }
+
+ const staticTSTypes = getTSTypes(
+ staticPropTypes,
+ (path) => path.parent.parent.parent.value.id.name
+ );
+
+ addFunctionTSTypes(source.find(j.FunctionDeclaration), tsTypes);
+ addFunctionTSTypes(source.find(j.FunctionExpression), tsTypes);
+ addFunctionTSTypes(source.find(j.ArrowFunctionExpression), tsTypes);
+ addClassTSTypes(source, tsTypes);
+ addClassTSTypes(source, staticTSTypes);
+
+ if (options.preservePropTypes === "none") {
+ propTypes.remove();
+ staticPropTypes.remove();
+ }
+
+ // Remove empty propTypes expressions and imports
+ cleanup(source, propTypes, staticPropTypes);
+
+ return source.toSource({ tabWidth: guessTabWidth(file.source) });
+}
diff --git a/tsconfig.codemod.json b/tsconfig.codemod.json
new file mode 100644
index 0000000..fcee9a7
--- /dev/null
+++ b/tsconfig.codemod.json
@@ -0,0 +1,40 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "composite": true,
+ "moduleResolution": "node",
+ "module": "NodeNext",
+ "target": "ES2015",
+ "lib": ["ESNext", "DOM"],
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "useDefineForClassFields": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "preserveWatchOutput": true,
+ "strict": true,
+ "strictNullChecks": true,
+ "incremental": true,
+ "noUncheckedIndexedAccess": true,
+ "noPropertyAccessFromIndexSignature": false,
+ "allowJs": true,
+ "outDir": "./dist",
+ "rootDir": "./transforms",
+ "moduleDetection": "auto"
+ },
+ "include": [
+ "./transforms/**/*.codemod.ts",
+ "./transforms/**/*.codemod.js",
+ "./transforms/**/*.codemod.tsx",
+ "./transforms/**/*.codemod.jsx"
+ ],
+ "exclude": ["node_modules", "./dist/**/*"],
+ "ts-node": {
+ "transpileOnly": true
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index 0d6dcc7..e305bf1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,9 @@
{
- "compilerOptions": {
- "moduleDetection": "force",
- "target": "ES2015"
- }
- }
\ No newline at end of file
+ "compilerOptions": {
+ "moduleDetection": "force",
+ "target": "ES2015"
+ },
+ "references": [
+ { "path": "./tsconfig.codemod.json" }
+ ]
+}