Skip to content

Commit

Permalink
Modify v2-text-component-interface codemod to cover more case (#1932)
Browse files Browse the repository at this point in the history
<!--
  How to write a good PR title:
- Follow [the Conventional Commits
specification](https://www.conventionalcommits.org/en/v1.0.0/).
  - Give as much context as necessary and as little as possible
  - Prefix it with [WIP] while it’s a work in progress
-->

## Self Checklist

- [x] I wrote a PR title in **English** and added an appropriate
**label** to the PR.
- [x] I wrote the commit message in **English** and to follow [**the
Conventional Commits
specification**](https://www.conventionalcommits.org/en/v1.0.0/).
- [x] I [added the
**changeset**](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md)
about the changes that needed to be released. (or didn't have to)
- [x] I wrote or updated **documentation** related to the changes. (or
didn't have to)
- [x] I wrote or updated **tests** related to the changes. (or didn't
have to)
- [x] I tested the changes in various browsers. (or didn't have to)
  - Windows: Chrome, Edge, (Optional) Firefox
  - macOS: Chrome, Edge, Safari, (Optional) Firefox

## Related Issue
<!-- Please link to issue if one exists -->

<!-- Fixes #0000 -->

- #1816

## Summary
<!-- Please brief explanation of the changes made -->

- `v2-text-component-interface` codemod 가 속성 값의 이름을 변경할 때(e.g.
typo={Typography.Size14} -> typo='14'), 컴포넌트 이름을 확인하지 않고 속성의 이름만 확인하도록
변경합니다.
- `sourceFile.fixUnusedImports()` 를 제거합니다.

## Details
<!-- Please elaborate description of the changes -->

- 기존의 변환 로직에서는 컴포넌트 이름이 정확히 `Text`이어야 하기 때문에 아래와 같은 경우를 변환할 수 없었습니다.
Text 컴포넌트는 사용처가 워낙 많아서 이름을 바꿔서 사용하는 곳도 많았습니다. 데스크 코드 기준으로 이런 경우가 100 여개
정도는 되었기 때문에 변환에 포함시키도록 합니다. `Text` 말고는 typo={Typography} 를 props 로 가지는
컴포넌트가 없기 때문에 이렇게 해도 문제가 없을 것으로 생각됩니다.

```tsx
import * as Styled from './some.styled.ts'
import { Typography } from '@channel.io/bezier-react'

<Styled.Title typo={Typography.Size14}>
  channel
</Styled.Title>
```

- `Typography` enum 을 제거하면서 사용처가 없어질 때, import 구문에서 제거하기 위해 ts-morph에서
제공하는 `sourceFile.fixUnusedImports()`를 사용했습니다. 하지만
`sourceFile.fixUnusedImports()`는 소스파일 전체를 순회해야하기 때문에 비용이 매우 크고, import
'styles.css' 와 같은 경우까지 제거해버려서 사이드 이펙트가 있습니다. 유틸함수로 만든
removeUnusedNamedImport 로 대체하였고, 시간이 2분 -> 20~30초 정도로 단축되는 것을 확인했습니다.
`sourceFile.fixUnusedImports()` 사용은 이제 지양하는 게 좋아보입니다.

### Breaking change? (Yes/No)
<!-- If Yes, please describe the impact and migration path for users -->

- No

## References
<!-- Please list any other resources or points the reviewer should be
aware of -->

- None
  • Loading branch information
yangwooseong authored Jan 19, 2024
1 parent a4647d3 commit 4538dd2
Show file tree
Hide file tree
Showing 13 changed files with 117 additions and 28 deletions.
6 changes: 6 additions & 0 deletions .changeset/bright-zoos-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@channel.io/bezier-codemod": minor
---

Changes in `v2-text-component-interface` codemod
Previously, both the component name and the name of the property were checked, but now only the name of the component property is checked.
7 changes: 5 additions & 2 deletions packages/bezier-codemod/src/shared/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
} from 'ts-morph'

import { renameEnumMember } from '../utils/enum.js'
import { hasNamedImportInImportDeclaration } from '../utils/import.js'
import {
hasNamedImportInImportDeclaration,
removeUnusedNamedImport,
} from '../utils/import.js'

type Name = string
type Member = string
Expand Down Expand Up @@ -32,7 +35,7 @@ export const transformEnumToStringLiteralInBezier = (sourceFile: SourceFile, enu
})

if (transformedEnumNames.length > 0) {
sourceFile.fixUnusedIdentifiers()
removeUnusedNamedImport(sourceFile, ['@channel.io/bezier-react'])
return true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from 'ts-morph'

import { getArrowFunctionsWithOneArgument } from '../../utils/function.js'
import { removeUnusedNamedImport } from '../../utils/import.js'

const cssVarByDuration: Record<string, string> = {
'TransitionDuration.S': 'var(--transition-s)',
Expand Down Expand Up @@ -63,7 +64,7 @@ const replaceTransitionsCSS = (sourceFile: SourceFile) => {
})

if (oldSourceFile !== sourceFile.getText()) {
sourceFile.fixUnusedIdentifiers()
removeUnusedNamedImport(sourceFile, ['@channel.io/bezier-react'])
}
}
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

export const OVERLAY_POSITION1 = {
zIndex: 'var(--z-index-modal)',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const replaceInputInterpolation = (sourceFile: SourceFile) => {

const isChanged = sourceFile.getText() !== oldSourceFileText
if (isChanged) {
removeUnusedNamedImport(sourceFile)
removeUnusedNamedImport(sourceFile, ['@channel.io/bezier-react'])
sourceFile.formatText({
semicolons: ts.SemicolonPreference.Remove,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const replaceTypographyInterpolation = (sourceFile: SourceFile) => {

const isChanged = sourceFile.getText() !== oldSourceFileText
if (isChanged) {
removeUnusedNamedImport(sourceFile)
removeUnusedNamedImport(sourceFile, ['@channel.io/bezier-react'])
sourceFile.formatText({
semicolons: ts.SemicolonPreference.Remove,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const replaceZIndexInterpolation = (sourceFile: SourceFile) => {

const isChanged = sourceFile.getText() !== oldSourceFileText
if (isChanged) {
removeUnusedNamedImport(sourceFile)
removeUnusedNamedImport(sourceFile, ['@channel.io/bezier-react'])
}
return isChanged
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Typography } from '@channel.io/bezier-react'
import * as Styled from './styled.ts'

export function Component () {
return (
<Styled.Title
typo={Typography.Size14}
marginAll={1}
marginTop={3}
marginRight={3}
marginBottom={3}
marginLeft={2}
marginHorizontal={3}
marginVertical={3}
>
text
</Styled.Title>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as Styled from './styled.ts'

export function Component () {
return (
<Styled.Title
typo="14"
marginAll={1}
marginTop={3}
marginRight={3}
marginBottom={3}
marginLeft={2}
marginHorizontal={3}
marginVertical={3}
>
text
</Styled.Title>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ describe('Text component transform', () => {
testTransformFunction(__dirname, 'text-component-props', textTransform)
})

it('should transform typography enum to string literal and margin properties to be shorthand when component name is not Text', () => {
testTransformFunction(__dirname, 'other-text-component-props', textTransform)
})

it('should transform properties in attrs object of styled component', () => {
testTransformFunction(__dirname, 'text-component-attrs', textTransform)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { type SourceFile } from 'ts-morph'

import {
type ComponentTransformMap,
type StyledAttrsTransformMap,
changeAttrProperty,
changeComponentProp,
} from '../../utils/component.js'
import { removeUnusedNamedImport } from '../../utils/import.js'

const STYLED_ATTRS_TRANSFORM_MAP: ComponentTransformMap = {
const STYLED_ATTRS_TRANSFORM_MAP: StyledAttrsTransformMap = {
Text: {
keyMapper: {
marginAll: 'margin',
Expand All @@ -29,11 +31,13 @@ const STYLED_ATTRS_TRANSFORM_MAP: ComponentTransformMap = {
}

const JSX_PROP_TRANSFORM_MAP: ComponentTransformMap = {
Text: {
keyMapper: {
keyMapper: {
Text: {
marginAll: 'margin',
},
valueMapper: {
},
valueMapper: {
typo: {
'{Typography.Size11}': '"11"',
'{Typography.Size12}': '"12"',
'{Typography.Size13}': '"13"',
Expand All @@ -51,10 +55,14 @@ const JSX_PROP_TRANSFORM_MAP: ComponentTransformMap = {
}

const transformTextComponentProps = (sourceFile: SourceFile) => {
const oldSourceFile = sourceFile.getText()

changeComponentProp(sourceFile, JSX_PROP_TRANSFORM_MAP)
changeAttrProperty(sourceFile, STYLED_ATTRS_TRANSFORM_MAP)

sourceFile.fixUnusedIdentifiers()
if (oldSourceFile !== sourceFile.getText()) {
removeUnusedNamedImport(sourceFile, ['@channel.io/bezier-react'])
}
}

export default transformTextComponentProps
46 changes: 31 additions & 15 deletions packages/bezier-codemod/src/utils/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,58 @@ import {
} from 'ts-morph'

type Component = string
type Props = string
type From = string
type To = string
export type ComponentTransformMap = Record<Component, { keyMapper?: Record<From, To>, valueMapper?: Record<From, To> }>
type FromToMap = Record<From, To>

export type StyledAttrsTransformMap = Record<Component, { keyMapper?: FromToMap, valueMapper?: FromToMap }>
export type ComponentTransformMap = { keyMapper?: Record<Component, FromToMap>, valueMapper?: Record<Props, FromToMap> }

const getName = (node: JsxSelfClosingElement | JsxOpeningElement) => node.getTagNameNode().getText()

export const changeComponentProp = (sourceFile: SourceFile, componentPropTransformMap: ComponentTransformMap) => {
const componentNames = new Set(Object.keys(componentPropTransformMap));
const keyMapper = componentPropTransformMap.keyMapper
if (!keyMapper) { return }
const componentNames = Object.keys(keyMapper);

([SyntaxKind.JsxSelfClosingElement, SyntaxKind.JsxOpeningElement] as const)
.flatMap((v) => sourceFile.getDescendantsOfKind(v))
.filter((node) => componentNames.has(getName(node)))
.filter((node) => componentNames.includes(getName(node)))
.forEach((jsxElement) => {
const elementName = getName(jsxElement)
const { keyMapper, valueMapper } = componentPropTransformMap[elementName]
const mapper = keyMapper[elementName]
jsxElement
.getDescendantsOfKind(SyntaxKind.JsxAttribute)
.forEach((attribute) => {
if (keyMapper) {
const propKeyFrom = attribute.getFirstChild()?.getText()
if (propKeyFrom && keyMapper[propKeyFrom]) {
attribute.getFirstChild()?.replaceWithText(keyMapper[propKeyFrom])
}
const from = attribute.getFirstChild()?.getText()
if (from && mapper[from]) {
attribute.getFirstChild()?.replaceWithText(mapper[from])
}
})
})

if (valueMapper) {
const propValueFrom = attribute.getLastChild()?.getText()
if (propValueFrom && valueMapper[propValueFrom]) {
attribute.getLastChild()?.replaceWithText(valueMapper[propValueFrom])
}
const valueMapper = componentPropTransformMap.valueMapper
if (!valueMapper) { return }
const propsNames = Object.keys(valueMapper);
([SyntaxKind.JsxSelfClosingElement, SyntaxKind.JsxOpeningElement] as const)
.flatMap((v) => sourceFile.getDescendantsOfKind(v))
.filter((v) => v.getDescendantsOfKind(SyntaxKind.JsxAttribute).some((attr) => propsNames.includes(attr.getFirstChild()?.getText() ?? '')))
.forEach((jsxElement) => {
jsxElement
.getDescendantsOfKind(SyntaxKind.JsxAttribute)
.forEach((attribute) => {
const prop = attribute.getFirstChild()?.getText() ?? ''
const valueFrom = attribute.getLastChild()?.getText()
const mapper = valueMapper[prop]
if (valueFrom && mapper?.[valueFrom]) {
attribute.getLastChild()?.replaceWithText(mapper[valueFrom])
}
})
})
}

export const changeAttrProperty = (sourceFile: SourceFile, transformMap: ComponentTransformMap) => {
export const changeAttrProperty = (sourceFile: SourceFile, transformMap: StyledAttrsTransformMap) => {
for (const component of Object.keys(transformMap)) {
const { keyMapper, valueMapper } = transformMap[component]

Expand Down
17 changes: 16 additions & 1 deletion packages/bezier-codemod/src/utils/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,26 @@ export const hasNamedImport = (sourceFile: SourceFile, namedImport: string) =>
export const removeNamedImport = (sourceFile: SourceFile, namedImport: string) =>
getNamedImport(sourceFile, namedImport)?.remove()

export const removeUnusedNamedImport = (sourceFile: SourceFile) => {
export const removeUnusedNamedImport = (sourceFile: SourceFile, importDeclarations?: string[]) => {
const trimQuoteAtBothEnds = (text: string) => text.match(/^['"](.*)['"]$/)?.[1]

sourceFile.getImportDeclarations()
.flatMap((declaration) => declaration.getNamedImports())
.filter((v) => (sourceFile.getDescendantsOfKind(SyntaxKind.Identifier).filter((_v) => _v.getText() === v.getText()).length === 1))
.forEach((v) => v.remove())

if (importDeclarations) {
sourceFile
.getImportDeclarations()
.filter((v) => importDeclarations.includes(
trimQuoteAtBothEnds(v.getModuleSpecifier().getText()) ?? ''),
)
.forEach((v) => {
if (!v.getImportClause()) {
v.remove()
}
})
}
}

export const renameNamedImport = (sourceFile: SourceFile, targets: string[], renameFn: (name: string) => string) => {
Expand Down

0 comments on commit 4538dd2

Please sign in to comment.