From ef01d96c9aeaaf1fdef724d946ca74bc6b413ff2 Mon Sep 17 00:00:00 2001 From: Gundu Mahidhar Reddy Date: Mon, 23 Sep 2024 10:37:29 +0800 Subject: [PATCH] [CCUBE-1554][MAHI]Codemod scripts for migrating text and mediaquery and their test cases --- codemods/migrate-mediaquery/data.ts | 20 ++++ codemods/migrate-mediaquery/index.ts | 95 +++++++++++++++++++ codemods/migrate-text/data.ts | 16 ++++ codemods/migrate-text/index.ts | 61 ++++++++++++ tests/codemod/migrate-mediaQuery/test-data.ts | 95 +++++++++++++++++++ .../migrate-mediaQuery/transformer.spec.tsx | 36 +++++++ tests/codemod/migrate-text/test-data.ts | 54 +++++++++++ .../codemod/migrate-text/transformer.spec.tsx | 36 +++++++ 8 files changed, 413 insertions(+) create mode 100644 codemods/migrate-mediaquery/data.ts create mode 100644 codemods/migrate-mediaquery/index.ts create mode 100644 codemods/migrate-text/data.ts create mode 100644 codemods/migrate-text/index.ts create mode 100644 tests/codemod/migrate-mediaQuery/test-data.ts create mode 100644 tests/codemod/migrate-mediaQuery/transformer.spec.tsx create mode 100644 tests/codemod/migrate-text/test-data.ts create mode 100644 tests/codemod/migrate-text/transformer.spec.tsx diff --git a/codemods/migrate-mediaquery/data.ts b/codemods/migrate-mediaquery/data.ts new file mode 100644 index 000000000..a923eeec6 --- /dev/null +++ b/codemods/migrate-mediaquery/data.ts @@ -0,0 +1,20 @@ +export const mediaQueryMap = { + MaxWidth: { + mobileS: "xxs", + mobileM: "xs", + mobileL: "sm", + tablet: "lg", + desktopM: "xl", + desktopL: "xl", + desktop4k: "xl", + }, + MinWidth: { + mobileS: "xs", + mobileM: "sm", + mobileL: "md", + tablet: "xl", + desktopM: "xxl", + desktopL: "xxl", + desktop4k: "xxl", + }, +}; diff --git a/codemods/migrate-mediaquery/index.ts b/codemods/migrate-mediaquery/index.ts new file mode 100644 index 000000000..93218a2d8 --- /dev/null +++ b/codemods/migrate-mediaquery/index.ts @@ -0,0 +1,95 @@ +import { API, FileInfo, JSCodeshift } from "jscodeshift"; +import { mediaQueryMap } from "./data"; + +export default function transformer(file: FileInfo, api: API, options: any) { + const j: JSCodeshift = api.jscodeshift; + const source = j(file.source); + + let isV2MediaQueryImport = false; + let isV2MediaWidthsUsed = false; + + source.find(j.ImportDeclaration).forEach((path) => { + const importPath = path.node.source.value; + + if (importPath === "@lifesg/react-design-system/v2_media") { + isV2MediaQueryImport = true; + + // loop over specifiers to rename V2_MediaQuery + if (path.node.specifiers && path.node.specifiers.length > 0) { + path.node.specifiers.forEach((specifier) => { + if (j.ImportSpecifier.check(specifier)) { + if (specifier.imported.name === "V2_MediaQuery") { + specifier.imported.name = "MediaQuery"; + if ( + specifier.local && + specifier.local.name === "V2_MediaQuery" + ) { + specifier.local.name = "MediaQuery"; + } + } else if ( + specifier.imported.name === "V2_MediaWidths" + ) { + isV2MediaWidthsUsed = true; + } + } + }); + + // change the import source path + path.node.source.value = "@lifesg/react-design-system/theme"; + } + } + }); + + if (isV2MediaWidthsUsed) { + let hasLoggedV2MediaWidthsWarning = false; + + source + .find(j.Identifier, { name: "V2_MediaWidths" }) + .forEach((path) => { + if (!hasLoggedV2MediaWidthsWarning) { + console.error( + `\x1b[31mDeprecated usage detected: V2_MediaWidths is deprecated and needs adjustment. File: ${file.path}\x1b[0m` + ); + hasLoggedV2MediaWidthsWarning = true; + } + }); + } + + if (isV2MediaQueryImport) { + // change all instances of V2_MediaQuery + source.find(j.Identifier, { name: "V2_MediaQuery" }).forEach((path) => { + path.node.name = "MediaQuery"; + }); + + // for nested member expression + source.find(j.MemberExpression).forEach((path) => { + const object = path.node.object; + const property = path.node.property; + + // checking the obj + if ( + j.MemberExpression.check(object) && // ensure obj is a MemberExpression + j.Identifier.check(object.object) && + object.object.name === "MediaQuery" && // check if MediaQuery + j.Identifier.check(object.property) && // max,min width + (object.property.name === "MaxWidth" || + object.property.name === "MinWidth") && + j.Identifier.check(property) + ) { + const queryType = object.property.name; + const mediaKey = property.name; + + // check if the mediaKey exists in the mediaQueryMap + if ( + mediaQueryMap[queryType] && + mediaQueryMap[queryType][mediaKey] + ) { + const newMediaKey = mediaQueryMap[queryType][mediaKey]; + property.name = newMediaKey; + } + } + }); + } + + return source.toSource(); +} diff --git a/codemods/migrate-text/data.ts b/codemods/migrate-text/data.ts new file mode 100644 index 000000000..a301729fe --- /dev/null +++ b/codemods/migrate-text/data.ts @@ -0,0 +1,16 @@ +export const textComponentMap = { + D1: "HeaderXXL", + D2: "HeaderXL", + D3: "HeaderMD", + D4: "HeaderSM", + H1: "HeaderLG", + H2: "HeaderMD", + H3: "HeaderSM", + H4: "HeaderXS", + H5: "HeaderXS", + H6: "HeaderXS", + DBody: "HeaderSM", + Body: "BodyBL", + BodySmall: "BodyLG", + XSmall: "LinkSM", +}; diff --git a/codemods/migrate-text/index.ts b/codemods/migrate-text/index.ts new file mode 100644 index 000000000..c0695d50d --- /dev/null +++ b/codemods/migrate-text/index.ts @@ -0,0 +1,61 @@ +import { API, FileInfo, JSCodeshift } from "jscodeshift"; +import { textComponentMap } from "./data"; + +export default function transformer(file: FileInfo, api: API, options: any) { + const j: JSCodeshift = api.jscodeshift; + const source = j(file.source); + + let isLifesgImport = false; + + // Check import declarations + source.find(j.ImportDeclaration).forEach((path) => { + const importPath = path.node.source.value; + + // If the import is from lifesg ds , set to true + if (importPath === "@lifesg/react-design-system/v2_text") { + isLifesgImport = true; + + // Check if specifiers exist and iterate over them + if (path.node.specifiers && path.node.specifiers.length > 0) { + path.node.specifiers.forEach((specifier) => { + if ( + j.ImportSpecifier.check(specifier) && + specifier.imported.name === "V2_Text" + ) { + specifier.imported.name = "Typography"; + if ( + specifier.local && + specifier.local.name === "V2_Text" + ) { + specifier.local.name = "Typography"; + } + + // Update the import path + path.node.source.value = + "@lifesg/react-design-system/typography"; + } + }); + } + } + }); + + // change if import from @lifesg/react-design-system + if (isLifesgImport) { + // update JSX and identifiers + source.find(j.JSXMemberExpression).forEach((path) => { + const { object, property } = path.node; + + // Change V2_text to typography + if (j.JSXIdentifier.check(object) && object.name === "V2_Text") { + object.name = "Typography"; + + // Map properties (e.g., Body -> BodyBL) + if (textComponentMap[property.name]) { + property.name = textComponentMap[property.name]; + } + } + }); + } + + return source.toSource(); +} diff --git a/tests/codemod/migrate-mediaQuery/test-data.ts b/tests/codemod/migrate-mediaQuery/test-data.ts new file mode 100644 index 000000000..91bd6a25e --- /dev/null +++ b/tests/codemod/migrate-mediaQuery/test-data.ts @@ -0,0 +1,95 @@ +export const inputCode = ` +import { V2_MediaQuery, V2_MediaWidths } from "@lifesg/react-design-system/v2_media"; + +const StyledContainer = styled.div\` + flex-grow: 1; + margin: 0 auto; + position: relative; + width: auto; + height: auto; + + \${(props) => { + if (props.stretch) { + return css\` + padding: 0 3rem; + \`; + } else { + return css\` + padding: 0 0.75rem; + /* Max width restrictions */ + max-width: 1320px; + + \${V2_MediaQuery.MaxWidth.desktopM} { + max-width: 1140px; + } + \`; + } + }} + \${V2_MediaQuery.MaxWidth.tablet} { + max-width: 720px; + } + \${V2_MediaQuery.MaxWidth.mobileL} { + width: 100%; + padding: 0; + max-width: unset; + } + + @media (max-width: \${V2_MediaWidths.mobileL}px) { + padding: 0; + max-width: 100%; + } + + @media (max-width: \${V2_MediaWidths.mobileL}px) { + padding: 0; + max-width: 100%; + } +\`; +`; + +export const expectedOutputCode = ` +import { MediaQuery, V2_MediaWidths } from "@lifesg/react-design-system/theme"; + +const StyledContainer = styled.div\` + flex-grow: 1; + margin: 0 auto; + position: relative; + width: auto; + height: auto; + + \${(props) => { + if (props.stretch) { + return css\` + padding: 0 3rem; + \`; + } else { + return css\` + padding: 0 0.75rem; + /* Max width restrictions */ + max-width: 1320px; + + \${MediaQuery.MaxWidth.xl} { + max-width: 1140px; + } + \`; + } + }} + \${MediaQuery.MaxWidth.lg} { + max-width: 720px; + } + \${MediaQuery.MaxWidth.sm} { + width: 100%; + padding: 0; + max-width: unset; + } + + @media (max-width: \${V2_MediaWidths.mobileL}px) { + padding: 0; + max-width: 100%; + } + + @media (max-width: \${V2_MediaWidths.mobileL}px) { + padding: 0; + max-width: 100%; + } +\`; +`; diff --git a/tests/codemod/migrate-mediaQuery/transformer.spec.tsx b/tests/codemod/migrate-mediaQuery/transformer.spec.tsx new file mode 100644 index 000000000..5e34ec2cc --- /dev/null +++ b/tests/codemod/migrate-mediaQuery/transformer.spec.tsx @@ -0,0 +1,36 @@ +import { execSync } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import { expectedOutputCode, inputCode } from "./test-data"; + +describe("Codemod Transformer for V2_Layout to Layout", () => { + const inputPath = path.join(__dirname, "input.tsx"); + const outputPath = path.join(__dirname, "output.tsx"); + + beforeAll(() => { + // Create sample input file for testing + jest.resetAllMocks(); + fs.writeFileSync(inputPath, inputCode); + }); + + afterAll(() => { + // Delete the files created for testing (comment this out to view files) + fs.unlinkSync(inputPath); + fs.unlinkSync(outputPath); + }); + + it("should transform V2_Layout components to Layout components and map props correctly", () => { + fs.copyFileSync(inputPath, outputPath); + + // Execute the jscodeshift command for the codemod + execSync( + `jscodeshift --parser=tsx -t ./codemods/migrate-mediaquery ${outputPath}` + ); + + // Check the transformed code + const transformedCode = fs.readFileSync(outputPath, "utf8"); + + // Compare the transformed code with the expected output + expect(transformedCode.trim()).toEqual(expectedOutputCode.trim()); + }); +}); diff --git a/tests/codemod/migrate-text/test-data.ts b/tests/codemod/migrate-text/test-data.ts new file mode 100644 index 000000000..7e9e5e64b --- /dev/null +++ b/tests/codemod/migrate-text/test-data.ts @@ -0,0 +1,54 @@ +export const inputCode = ` + + +import { V2_Text } from "@lifesg/react-design-system/v2_text"; + +const ExampleComponent = () => ( +
+ This is body text + This is a heading + This is smaller body text + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Morbi euismod quam eget ex tincidunt dapibus. Donec vitae + leo vehicula, fermentum urna vitae, gravida ex. + + + Aenean imperdiet faucibus velit, eu maximus libero facilisis + ut. Donec nulla nisi, fermentum eget lorem at, feugiat + ultricies ex. Aliquam volutpat nibh non suscipit rhoncus. + + +
+); + +export default ExampleComponent; +`; + +export const expectedOutputCode = ` + +import { Typography } from "@lifesg/react-design-system/typography"; + +const ExampleComponent = () => ( +
+ This is body text + This is a heading + This is smaller body text + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Morbi euismod quam eget ex tincidunt dapibus. Donec vitae + leo vehicula, fermentum urna vitae, gravida ex. + + + Aenean imperdiet faucibus velit, eu maximus libero facilisis + ut. Donec nulla nisi, fermentum eget lorem at, feugiat + ultricies ex. Aliquam volutpat nibh non suscipit rhoncus. + + +
+); + +export default ExampleComponent; +`; diff --git a/tests/codemod/migrate-text/transformer.spec.tsx b/tests/codemod/migrate-text/transformer.spec.tsx new file mode 100644 index 000000000..0f1dd6001 --- /dev/null +++ b/tests/codemod/migrate-text/transformer.spec.tsx @@ -0,0 +1,36 @@ +import { execSync } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import { expectedOutputCode, inputCode } from "./test-data"; + +describe("Codemod Transformer for V2_Text to Typography", () => { + const inputPath = path.join(__dirname, "input.tsx"); + const outputPath = path.join(__dirname, "output.tsx"); + + beforeAll(() => { + // create sample input file for testing + jest.resetAllMocks(); + fs.writeFileSync(inputPath, inputCode); + }); + + afterAll(() => { + // delete the files created for testing (comment this out to view files) + fs.unlinkSync(inputPath); + fs.unlinkSync(outputPath); + }); + + it("should transform V2_Text components to Typography components", () => { + fs.copyFileSync(inputPath, outputPath); + + // Execute the jscodeshift command for the codemod + execSync( + `jscodeshift --parser=tsx -t ./codemods/migrate-text ${outputPath}` + ); + + // Check the transformed code + const transformedCode = fs.readFileSync(outputPath, "utf8"); + + // Compare the transformed code with the expected output + expect(transformedCode.trim()).toEqual(expectedOutputCode.trim()); + }); +});