diff --git a/packages/codemod/package.json b/packages/codemod/package.json index bd24f7a535..aefb3a5caf 100644 --- a/packages/codemod/package.json +++ b/packages/codemod/package.json @@ -34,9 +34,12 @@ "devDependencies": { "@commercetools-frontend/application-components": "workspace:*", "@emotion/react": "^11.11.4", + "@jest/globals": "29.7.0", "@tsconfig/node16": "^16.1.1", "@types/glob": "8.1.0", "@types/jscodeshift": "0.11.11", + "@types/prop-types": "^15.7.5", + "prop-types": "15.8.1", "rimraf": "5.0.7", "typescript": "5.0.4" }, diff --git a/packages/codemod/test/__snapshots__/transforms.spec.ts.snap b/packages/codemod/test/__snapshots__/transforms.spec.ts.snap index 2f96ae8b4b..be9ba4caa4 100644 --- a/packages/codemod/test/__snapshots__/transforms.spec.ts.snap +++ b/packages/codemod/test/__snapshots__/transforms.spec.ts.snap @@ -1,6 +1,141 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`testing transform "remove-deprecated-modal-level-props" transforms correctly 1`] = ` +exports[`testing transform "react-default-props-migration" transforms correctly: react-default-props/simple-arrow-function.jsx 1`] = ` +"import * as PropTypes from 'prop-types'; + +const MyComponent = ({ foo = 'bar', ...props }) => { + return
{foo}
; +}; +MyComponent.propTypes = { + foo: PropTypes.string, +};" +`; + +exports[`testing transform "react-default-props-migration" transforms correctly: react-default-props/simple-arrow-function.tsx 1`] = ` +"type TMyComponentProps = { + foo?: string; +}; + +const MyComponent = ({ foo = 'bar', ...props }: TMyComponentProps) => { + return
{foo}
; +};" +`; + +exports[`testing transform "react-default-props-migration" transforms correctly: react-default-props/simple-classic-function.jsx 1`] = ` +"import * as PropTypes from 'prop-types'; + +function MyComponent({ foo = 'bar', ...props }) { + return
{foo}
; +} +MyComponent.propTypes = { + foo: PropTypes.string, +};" +`; + +exports[`testing transform "react-default-props-migration" transforms correctly: react-default-props/simple-classic-function.tsx 1`] = ` +"type TMyComponentProps = { + foo?: string; +}; + +function MyComponent({ foo = 'bar', ...props }: TMyComponentProps) { + return
{foo}
; +}" +`; + +exports[`testing transform "react-default-props-migration" transforms correctly: react-default-props/with-already-destructured-props.jsx 1`] = ` +"import * as PropTypes from 'prop-types'; + +const MyComponent = ({ foo = 'bar', baz, ...props }) => { + return ( + + ); +}; +MyComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string, + baz: PropTypes.string, +};" +`; + +exports[`testing transform "react-default-props-migration" transforms correctly: react-default-props/with-already-destructured-props.tsx 1`] = ` +"type TMyComponentProps = { + foo?: string; + bar: string; + baz: string; +}; + +const MyComponent = ({ foo = 'bar', baz, ...props }: TMyComponentProps) => { + return ( + + ); +};" +`; + +exports[`testing transform "react-default-props-migration" transforms correctly: react-default-props/with-subcomponent.jsx 1`] = ` +"import * as PropTypes from 'prop-types'; + +function MySubcomponent(props) { + return ( + + ); +} +MySubcomponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string, +}; + +function MyComponent({ foo = 'bar', ...props }) { + return ( +
+ +
+ ); +} +MyComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string, +};" +`; + +exports[`testing transform "react-default-props-migration" transforms correctly: react-default-props/with-subcomponent.tsx 1`] = ` +"type MySubcomponentProps = { + foo: string; + bar: string; +}; +function MySubcomponent(props: MySubcomponentProps) { + return ( + + ); +} + +type TMyComponentProps = { + foo?: string; + bar: string; +}; +function MyComponent({ foo = 'bar', ...props }: TMyComponentProps) { + return ( +
+ +
+ ); +}" +`; + +exports[`testing transform "remove-deprecated-modal-level-props" transforms correctly: remove-deprecated-modal-level-props.tsx 1`] = ` "import { InfoModalPage, FormModalPage, @@ -43,9 +178,9 @@ function Modal() { export default Modal;" `; -exports[`testing transform "rename-js-to-jsx" transforms correctly 1`] = `""`; +exports[`testing transform "rename-js-to-jsx" transforms correctly: rename-js-to-jsx.js 1`] = `""`; -exports[`testing transform "rename-mod-css-to-module-css" transforms correctly 1`] = ` +exports[`testing transform "rename-mod-css-to-module-css" transforms correctly: rename-mod-css-to-module-css.jsx 1`] = ` "// eslint-disable-next-line import/extensions, import/no-unresolved, no-unused-vars import styles from './styles.module.css';" `; diff --git a/packages/codemod/test/fixtures/react-default-props/simple-arrow-function.jsx b/packages/codemod/test/fixtures/react-default-props/simple-arrow-function.jsx new file mode 100644 index 0000000000..6a9c84cf37 --- /dev/null +++ b/packages/codemod/test/fixtures/react-default-props/simple-arrow-function.jsx @@ -0,0 +1,11 @@ +import * as PropTypes from 'prop-types'; + +const MyComponent = (props) => { + return
{props.foo}
; +}; +MyComponent.defaultProps = { + foo: 'bar', +}; +MyComponent.propTypes = { + foo: PropTypes.string, +}; diff --git a/packages/codemod/test/fixtures/react-default-props/simple-arrow-function.tsx b/packages/codemod/test/fixtures/react-default-props/simple-arrow-function.tsx new file mode 100644 index 0000000000..2875d11fdb --- /dev/null +++ b/packages/codemod/test/fixtures/react-default-props/simple-arrow-function.tsx @@ -0,0 +1,11 @@ +type TMyComponentProps = { + foo: string; +}; + +const MyComponent = (props: TMyComponentProps) => { + return
{props.foo}
; +}; + +MyComponent.defaultProps = { + foo: 'bar', +}; diff --git a/packages/codemod/test/fixtures/react-default-props/simple-classic-function.jsx b/packages/codemod/test/fixtures/react-default-props/simple-classic-function.jsx new file mode 100644 index 0000000000..5568aa4439 --- /dev/null +++ b/packages/codemod/test/fixtures/react-default-props/simple-classic-function.jsx @@ -0,0 +1,11 @@ +import * as PropTypes from 'prop-types'; + +function MyComponent(props) { + return
{props.foo}
; +} +MyComponent.defaultProps = { + foo: 'bar', +}; +MyComponent.propTypes = { + foo: PropTypes.string, +}; diff --git a/packages/codemod/test/fixtures/react-default-props/simple-classic-function.tsx b/packages/codemod/test/fixtures/react-default-props/simple-classic-function.tsx new file mode 100644 index 0000000000..ffa95843f9 --- /dev/null +++ b/packages/codemod/test/fixtures/react-default-props/simple-classic-function.tsx @@ -0,0 +1,11 @@ +type TMyComponentProps = { + foo: string; +}; + +function MyComponent(props: TMyComponentProps) { + return
{props.foo}
; +} + +MyComponent.defaultProps = { + foo: 'bar', +}; diff --git a/packages/codemod/test/fixtures/react-default-props/with-already-destructured-props.jsx b/packages/codemod/test/fixtures/react-default-props/with-already-destructured-props.jsx new file mode 100644 index 0000000000..5d6c0ed495 --- /dev/null +++ b/packages/codemod/test/fixtures/react-default-props/with-already-destructured-props.jsx @@ -0,0 +1,19 @@ +import * as PropTypes from 'prop-types'; + +const MyComponent = ({ baz, ...props }) => { + return ( + + ); +}; +MyComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string, + baz: PropTypes.string, +}; +MyComponent.defaultProps = { + foo: 'bar', +}; diff --git a/packages/codemod/test/fixtures/react-default-props/with-already-destructured-props.tsx b/packages/codemod/test/fixtures/react-default-props/with-already-destructured-props.tsx new file mode 100644 index 0000000000..af2dd5e3c7 --- /dev/null +++ b/packages/codemod/test/fixtures/react-default-props/with-already-destructured-props.tsx @@ -0,0 +1,19 @@ +type TMyComponentProps = { + foo: string; + bar: string; + baz: string; +}; + +const MyComponent = ({ baz, ...props }: TMyComponentProps) => { + return ( + + ); +}; + +MyComponent.defaultProps = { + foo: 'bar', +}; diff --git a/packages/codemod/test/fixtures/react-default-props/with-subcomponent.jsx b/packages/codemod/test/fixtures/react-default-props/with-subcomponent.jsx new file mode 100644 index 0000000000..3fe07d2162 --- /dev/null +++ b/packages/codemod/test/fixtures/react-default-props/with-subcomponent.jsx @@ -0,0 +1,29 @@ +import * as PropTypes from 'prop-types'; + +function MySubcomponent(props) { + return ( + + ); +} +MySubcomponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string, +}; + +function MyComponent(props) { + return ( +
+ +
+ ); +} +MyComponent.propTypes = { + foo: PropTypes.string, + bar: PropTypes.string, +}; +MyComponent.defaultProps = { + foo: 'bar', +}; diff --git a/packages/codemod/test/fixtures/react-default-props/with-subcomponent.tsx b/packages/codemod/test/fixtures/react-default-props/with-subcomponent.tsx new file mode 100644 index 0000000000..269e47702a --- /dev/null +++ b/packages/codemod/test/fixtures/react-default-props/with-subcomponent.tsx @@ -0,0 +1,27 @@ +type MySubcomponentProps = { + foo: string; + bar: string; +}; +function MySubcomponent(props: MySubcomponentProps) { + return ( + + ); +} + +type TMyComponentProps = { + foo: string; + bar: string; +}; +function MyComponent(props: TMyComponentProps) { + return ( +
+ +
+ ); +} +MyComponent.defaultProps = { + foo: 'bar', +}; diff --git a/packages/codemod/test/test-utils.ts b/packages/codemod/test/test-utils.ts new file mode 100644 index 0000000000..56b514d928 --- /dev/null +++ b/packages/codemod/test/test-utils.ts @@ -0,0 +1,100 @@ +import { readFileSync } from 'node:fs'; +import { extname } from 'node:path'; +import { expect } from '@jest/globals'; +import { API, FileInfo, Options } from 'jscodeshift'; + +type TTransformerModule = + | { + default: ( + fileInfo: Partial, + api: API, + options: Options + ) => Promise | string; + parser: string; + } + | (( + fileInfo: Partial, + api: API, + options: Options + ) => Promise | string); + +type TApplyTransformParams = { + transformerModule: TTransformerModule; + transformerOptions?: Options; + codeToTransform: Partial; + testOptions?: { + parser?: string; + }; +}; + +function applyTransform({ + transformerModule, + transformerOptions, + codeToTransform, + testOptions = {}, +}: TApplyTransformParams) { + // Handle ES6 modules using default export for the transform + const transform = + 'default' in transformerModule + ? transformerModule.default + : transformerModule; + const moduleParser = + 'default' in transformerModule ? transformerModule.parser : null; + + // Jest resets the module registry after each test, so we need to always get + // a fresh copy of jscodeshift on every test run. + let jscodeshift = require('jscodeshift'); + if (testOptions.parser || moduleParser) { + jscodeshift = jscodeshift.withParser(testOptions.parser || moduleParser); + } + + const transformationResult = transform( + codeToTransform, + { + jscodeshift, + j: jscodeshift, + stats: () => {}, + report: (msg: string) => console.log(msg), // Add the missing report function + }, + transformerOptions || {} + ); + + if (transformationResult instanceof Promise) { + return transformationResult.then((result) => (result || '').trim()); + } + + return (transformationResult || '').trim(); +} + +type TRunSnapshotTestParams = { + transformerModule: TTransformerModule; + transformerOptions?: Options; + codeToTransformPath: string; +}; +export function runSnapshotTest({ + transformerModule, + transformerOptions, + codeToTransformPath, +}: TRunSnapshotTestParams): Promise | undefined { + const source = readFileSync(codeToTransformPath, 'utf8'); + const transformationResult = applyTransform({ + transformerModule, + transformerOptions, + codeToTransform: { + path: codeToTransformPath, + source, + }, + testOptions: { + parser: extname(codeToTransformPath).slice(1), + }, + }); + + if (transformationResult instanceof Promise) { + return transformationResult.then((result) => { + expect(result).toMatchSnapshot(); + }); + } + + expect(transformationResult).toMatchSnapshot(); + return undefined; +} diff --git a/packages/codemod/test/transforms.spec.ts b/packages/codemod/test/transforms.spec.ts index 48f53d809d..42973431b3 100644 --- a/packages/codemod/test/transforms.spec.ts +++ b/packages/codemod/test/transforms.spec.ts @@ -3,8 +3,7 @@ jest.autoMockOff(); import fs from 'fs'; import path from 'path'; -// @ts-ignore -import { runSnapshotTest } from 'jscodeshift/dist/testUtils'; +import { runSnapshotTest } from './test-utils'; const fixturesPath = path.join(__dirname, 'fixtures'); @@ -22,6 +21,14 @@ describe.each` ${'remove-deprecated-modal-level-props'} | ${'remove-deprecated-modal-level-props.tsx'} ${'rename-js-to-jsx'} | ${'rename-js-to-jsx.js'} ${'rename-mod-css-to-module-css'} | ${'rename-mod-css-to-module-css.jsx'} + ${'react-default-props-migration'} | ${'react-default-props/simple-classic-function.jsx'} + ${'react-default-props-migration'} | ${'react-default-props/simple-classic-function.tsx'} + ${'react-default-props-migration'} | ${'react-default-props/simple-arrow-function.jsx'} + ${'react-default-props-migration'} | ${'react-default-props/simple-arrow-function.tsx'} + ${'react-default-props-migration'} | ${'react-default-props/with-subcomponent.jsx'} + ${'react-default-props-migration'} | ${'react-default-props/with-subcomponent.tsx'} + ${'react-default-props-migration'} | ${'react-default-props/with-already-destructured-props.jsx'} + ${'react-default-props-migration'} | ${'react-default-props/with-already-destructured-props.tsx'} `('testing transform "$transformName"', ({ transformName, fixtureName }) => { // Assumes transform is one level up from __tests__ directory const module = require(path.join( @@ -32,6 +39,7 @@ describe.each` const inputPath = path.join(fixturesPath, fixtureName); beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(); switch (transformName) { case 'rename-js-to-jsx': if (!doesFileExist(inputPath)) { @@ -52,12 +60,10 @@ describe.each` } }); - it('transforms correctly', () => { - const source = fs.readFileSync(inputPath, 'utf8'); - - runSnapshotTest(module, null, { - source, - path: inputPath, + it(`transforms correctly: ${fixtureName}`, async () => { + return runSnapshotTest({ + transformerModule: module, + codeToTransformPath: inputPath, }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 260e32820e..9956dc0966 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2087,6 +2087,9 @@ importers: '@emotion/react': specifier: ^11.11.4 version: 11.11.4(@types/react@17.0.83)(react@17.0.2) + '@jest/globals': + specifier: 29.7.0 + version: 29.7.0 '@tsconfig/node16': specifier: ^16.1.1 version: 16.1.1 @@ -2096,6 +2099,12 @@ importers: '@types/jscodeshift': specifier: 0.11.11 version: 0.11.11 + '@types/prop-types': + specifier: ^15.7.5 + version: 15.7.5 + prop-types: + specifier: 15.8.1 + version: 15.8.1 rimraf: specifier: 5.0.7 version: 5.0.7 @@ -13696,7 +13705,7 @@ packages: jest-haste-map: 29.7.0 jest-regex-util: 29.6.3 jest-util: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.8 pirates: 4.0.5 slash: 3.0.0 write-file-atomic: 4.0.2 @@ -18018,7 +18027,6 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.1.1 - dev: false /breakword@1.0.5: resolution: {integrity: sha512-ex5W9DoOQ/LUEU3PMdLs9ua/CYZl1678NUkKOdUSi8Aw5F1idieaiRURCBFJCwVcrD1J8Iy3vfWSloaMwO2qFg==} @@ -21473,7 +21481,6 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 - dev: false /finalhandler@1.1.2: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} @@ -24079,7 +24086,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@babel/core': 7.25.2 - '@babel/generator': 7.24.5 + '@babel/generator': 7.25.6 '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) '@babel/plugin-syntax-typescript': 7.25.4(@babel/core@7.25.2) '@babel/types': 7.25.6 @@ -24097,7 +24104,7 @@ packages: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.6.3 + semver: 7.6.2 transitivePeerDependencies: - supports-color @@ -25701,7 +25708,6 @@ packages: dependencies: braces: 3.0.3 picomatch: 2.3.1 - dev: false /mime-db@1.33.0: resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==}