From 6fd50752fa23d7e38b0c502a89b34a51b48d3f21 Mon Sep 17 00:00:00 2001 From: Gundu Mahidhar Reddy Date: Thu, 3 Oct 2024 15:34:52 +0800 Subject: [PATCH] [CCUBE-1558][MAHI]Migrate colour and layout codemod scripts with its test cases --- codemods/migrate-colour/data.ts | 59 ++++++++ codemods/migrate-colour/index.ts | 138 ++++++++++++++++++ codemods/migrate-layout/data.ts | 5 + codemods/migrate-layout/index.ts | 103 +++++++++++++ tests/codemod/migrate-color/test-data.ts | 19 +++ .../migrate-color/transformer.spec.tsx | 36 +++++ tests/codemod/migrate-layout/test-data.ts | 45 ++++++ .../migrate-layout/transformer.spec.tsx | 36 +++++ 8 files changed, 441 insertions(+) create mode 100644 codemods/migrate-colour/data.ts create mode 100644 codemods/migrate-colour/index.ts create mode 100644 codemods/migrate-layout/data.ts create mode 100644 codemods/migrate-layout/index.ts create mode 100644 tests/codemod/migrate-color/test-data.ts create mode 100644 tests/codemod/migrate-color/transformer.spec.tsx create mode 100644 tests/codemod/migrate-layout/test-data.ts create mode 100644 tests/codemod/migrate-layout/transformer.spec.tsx diff --git a/codemods/migrate-colour/data.ts b/codemods/migrate-colour/data.ts new file mode 100644 index 000000000..221807592 --- /dev/null +++ b/codemods/migrate-colour/data.ts @@ -0,0 +1,59 @@ +export const lifesgMapping = { + Secondary: "primary-40", + PrimaryDark: "primary-40", + Primary: "primary-50", + "Accent.Light[1]": "primary-60", + "Accent.Light[2]": "primary-70", + "Accent.Light[3]": "primary-80", + "Accent.Light[4]": "primary-90", + "Accent.Light[5]": "primary-95", + "Accent.Light[6]": "primary-100", + "Accent.Dark[1]": "secondary-40", + "Accent.Dark[2]": "secondary-50", + "Accent.Dark[3]": "secondary-60", +}; + +export const bookingSgMapping = { + PrimaryDark: "primary-40", + Primary: "primary-50", + "Brand[1]": "brand-50", + "Brand[2]": "brand-60", + "Brand[3]": "brand-70", + "Brand[4]": "brand-80", + "Brand[5]": "brand-90", + "Brand[6]": "brand-95", + "Accent.Light[1]": "primary-60", + "Accent.Dark[1]": "secondary-40", + "Accent.Dark[2]": "secondary-60", + "Accent.Dark[3]": "secondary-70", + "Neutral[1]": "neutral-20", + "Neutral[2]": "neutral-30", + "Neutral[3]": "neutral-50", + "Neutral[4]": "neutral-60", + "Neutral[5]": "neutral-90", + "Neutral[6]": "neutral-95", + "Neutral[7]": "neutral-100", + "Blue[1]": "information-40", + "Blue[2]": "information-50", + "Blue[3]": "information-70", +}; + +export const mylegacyMapping = { + PrimaryDark: "primary-40", + Primary: "primary-50", + "Accent.Light[1]": "primary-60", + "Accent.Light[2]": "primary-80", + "Accent.Light[3]": "primary-90", + "Accent.Light[4]": "primary-95", + "Accent.Light[5]": "primary-100", + "Accent.Light[6]": "primary-100", +}; + +export const ccubeMapping = { + "Brand[2]": "brand-50", + "Brand[1]": "brand-60", + Primary: "primary-50", + Secondary: "secondary-50", +}; + +export const rbsMapping = {}; diff --git a/codemods/migrate-colour/index.ts b/codemods/migrate-colour/index.ts new file mode 100644 index 000000000..c8993cebe --- /dev/null +++ b/codemods/migrate-colour/index.ts @@ -0,0 +1,138 @@ +import { API, FileInfo, JSCodeshift } from "jscodeshift"; +import { + bookingSgMapping, + ccubeMapping, + lifesgMapping, + mylegacyMapping, + rbsMapping, +} from "./data"; + +const IMPORT_PATHS = { + V2_COLOR: "@lifesg/react-design-system/v2_color", + THEME: "@lifesg/react-design-system/theme", +}; + +const IMPORT_SPECIFIERS = { + V2_COLOR: "V2_Color", + COLOUR: "Colour", +}; + +const MEMBER_EXPRESSION_PROPERTIES = { + PRIMITIVE: "Primitive", +}; + +const COLOR_MAPPINGS = { + lifesg: lifesgMapping, + bookingsg: bookingSgMapping, + mylegacy: mylegacyMapping, + ccube: ccubeMapping, + rbs: rbsMapping, +}; + +export default function transformer(file: FileInfo, api: API, options: any) { + const j: JSCodeshift = api.jscodeshift; + const source = j(file.source); + + let isv2ColorImport = false; + + // Determine which Colour mapping to use + const color_mapping = + COLOR_MAPPINGS[options.mapping] || COLOR_MAPPINGS.lifesg; + + //Update Colour usage post mapping + const replaceWithColorPrimitive = (path: any, new_color_value: string) => { + path.replace( + j.memberExpression( + j.memberExpression( + j.identifier(IMPORT_SPECIFIERS.COLOUR), + j.identifier(MEMBER_EXPRESSION_PROPERTIES.PRIMITIVE) + ), + j.literal(new_color_value) + ) + ); + }; + + source.find(j.ImportDeclaration).forEach((path) => { + const import_path = path.node.source.value; + + if (import_path === IMPORT_PATHS.V2_COLOR) { + isv2ColorImport = true; + + // Update imports + if (path.node.specifiers && path.node.specifiers.length > 0) { + path.node.specifiers.forEach((specifier) => { + if (j.ImportSpecifier.check(specifier)) { + if ( + specifier.imported.name === + IMPORT_SPECIFIERS.V2_COLOR + ) { + specifier.imported.name = IMPORT_SPECIFIERS.COLOUR; + if ( + specifier.local && + specifier.local.name === + IMPORT_SPECIFIERS.V2_COLOR + ) { + specifier.local.name = IMPORT_SPECIFIERS.COLOUR; + } + } + } + }); + + path.node.source.value = IMPORT_PATHS.THEME; + } + } + }); + + if (isv2ColorImport) { + // Update all instances of 'V2_Color' to 'Colour' + source + .find(j.Identifier, { name: IMPORT_SPECIFIERS.V2_COLOR }) + .forEach((path) => { + path.node.name = IMPORT_SPECIFIERS.COLOUR; + }); + + // Map V2 Color usage to V3 Colour + source.find(j.MemberExpression).forEach((path) => { + let currentPath = path.node; + const propertyNameParts: string[] = []; + let index: any = null; + let startsWithColour = false; + + while (j.MemberExpression.check(currentPath)) { + const property = currentPath.property; + const object = currentPath.object; + + if (j.Literal.check(property)) { + index = property.value; + } else if (j.Identifier.check(property)) { + propertyNameParts.unshift(property.name); + } + + if (j.MemberExpression.check(object)) { + currentPath = object; + } else if (j.Identifier.check(object)) { + if (object.name === IMPORT_SPECIFIERS.COLOUR) { + startsWithColour = true; + } + break; + } else { + break; + } + } + + if (startsWithColour) { + let property_name = propertyNameParts.join("."); + if (index !== null) { + property_name += `[${index}]`; + } + + const newColorValue = color_mapping[property_name]; + if (newColorValue) { + replaceWithColorPrimitive(path, newColorValue); + } + } + }); + } + + return source.toSource(); +} diff --git a/codemods/migrate-layout/data.ts b/codemods/migrate-layout/data.ts new file mode 100644 index 000000000..ee3a26a66 --- /dev/null +++ b/codemods/migrate-layout/data.ts @@ -0,0 +1,5 @@ +export const propMapping = { + desktopCols: "xlCols", + tabletCols: "lgCols", + mobileCols: "smCols", +}; diff --git a/codemods/migrate-layout/index.ts b/codemods/migrate-layout/index.ts new file mode 100644 index 000000000..9fa890fa0 --- /dev/null +++ b/codemods/migrate-layout/index.ts @@ -0,0 +1,103 @@ +import { API, FileInfo, JSCodeshift } from "jscodeshift"; +import { propMapping } from "./data"; + +const IMPORT_PATHS = { + V2_LAYOUT: "@lifesg/react-design-system/v2_layout", + LAYOUT: "@lifesg/react-design-system/layout", +}; + +const IMPORT_SPECIFIERS = { + V2_LAYOUT: "V2_Layout", + LAYOUT: "Layout", +}; + +export default function transformer(file: FileInfo, api: API, options: any) { + const j: JSCodeshift = api.jscodeshift; + const source = j(file.source); + + let isLifesgImport = false; + + source.find(j.ImportDeclaration).forEach((path) => { + const importPath = path.node.source.value; + + if (importPath === IMPORT_PATHS.V2_LAYOUT) { + isLifesgImport = true; + + // Update import path + if (path.node.specifiers && path.node.specifiers.length > 0) { + path.node.specifiers.forEach((specifier) => { + if ( + j.ImportSpecifier.check(specifier) && + specifier.imported.name === IMPORT_SPECIFIERS.V2_LAYOUT + ) { + specifier.imported.name = IMPORT_SPECIFIERS.LAYOUT; + if ( + specifier.local && + specifier.local.name === IMPORT_SPECIFIERS.V2_LAYOUT + ) { + specifier.local.name = IMPORT_SPECIFIERS.LAYOUT; + } + + path.node.source.value = IMPORT_PATHS.LAYOUT; + } + }); + } + } + }); + + if (isLifesgImport) { + // Update V2_Layout to Layout + source.find(j.JSXMemberExpression).forEach((path) => { + const { object } = path.node; + + if ( + j.JSXIdentifier.check(object) && + object.name === IMPORT_SPECIFIERS.V2_LAYOUT + ) { + object.name = IMPORT_SPECIFIERS.LAYOUT; + } + }); + + // Update Layout props to its V3 Mapped version + source.find(j.JSXOpeningElement).forEach((path) => { + const { name, attributes } = path.node; + + if ( + j.JSXMemberExpression.check(name) && + j.JSXIdentifier.check(name.object) && + name.object.name === IMPORT_SPECIFIERS.LAYOUT + ) { + if (attributes && attributes.length > 0) { + attributes.forEach((attribute) => { + if ( + j.JSXAttribute.check(attribute) && + j.JSXIdentifier.check(attribute.name) + ) { + const oldPropName = attribute.name.name; + const newPropName = propMapping[oldPropName]; + + if (newPropName) { + attribute.name.name = newPropName; + } + } + }); + } + } + }); + + source.find(j.JSXElement).forEach((path) => { + const openingElement = path.node.openingElement; + const { name } = openingElement; + if ( + j.JSXMemberExpression.check(name) && + j.JSXIdentifier.check(name.object) + ) { + if (name.object.name === IMPORT_SPECIFIERS.V2_LAYOUT) { + name.object.name = IMPORT_SPECIFIERS.LAYOUT; + } + } + }); + } + + return source.toSource(); +} diff --git a/tests/codemod/migrate-color/test-data.ts b/tests/codemod/migrate-color/test-data.ts new file mode 100644 index 000000000..91a0123d2 --- /dev/null +++ b/tests/codemod/migrate-color/test-data.ts @@ -0,0 +1,19 @@ +export const inputCode = ` +import { V2_Color } from '@lifesg/react-design-system/v2_color'; + +const styles = css\` + color: \${V2_Color.Primary}; + border-color: \${V2_Color.Accent.Dark[3]}; + text-shadow: 1px 1px \${V2_Color.Accent.Light[1]}; +\`; +`; + +export const expectedOutputCode = ` +import { Colour } from "@lifesg/react-design-system/theme"; + +const styles = css\` + color: \${Colour.Primitive["primary-50"]}; + border-color: \${Colour.Primitive["secondary-60"]}; + text-shadow: 1px 1px \${Colour.Primitive["primary-60"]}; +\`; +`; diff --git a/tests/codemod/migrate-color/transformer.spec.tsx b/tests/codemod/migrate-color/transformer.spec.tsx new file mode 100644 index 000000000..3f55006b3 --- /dev/null +++ b/tests/codemod/migrate-color/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_Color to Colour", () => { + 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_Color tokens to Colour tokens and map theme correctly", () => { + fs.copyFileSync(inputPath, outputPath); + + // Execute the jscodeshift command for the codemod + execSync( + `jscodeshift --parser=tsx --verbose=2 -t ./codemods/migrate-colour --mapping=lifesg ${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-layout/test-data.ts b/tests/codemod/migrate-layout/test-data.ts new file mode 100644 index 000000000..3806f9dee --- /dev/null +++ b/tests/codemod/migrate-layout/test-data.ts @@ -0,0 +1,45 @@ +export const inputCode = ` +import { V2_Layout } from "@lifesg/react-design-system/v2_layout"; + +
+ + {children} + + {content} + + + + {children} + + + + + + {content} + + +
+`; + +export const expectedOutputCode = ` +import { Layout } from "@lifesg/react-design-system/layout"; + +
+ + {children} + + {content} + + + + {children} + + + + + + {content} + + +
+`; diff --git a/tests/codemod/migrate-layout/transformer.spec.tsx b/tests/codemod/migrate-layout/transformer.spec.tsx new file mode 100644 index 000000000..7d6ec4c53 --- /dev/null +++ b/tests/codemod/migrate-layout/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 component to Layout component", () => { + 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-layout ${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()); + }); +});