@wishket/design-system 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/Components/Base/Layouts/Box/Box.d.ts +5 -5
- package/dist/Components/Base/Layouts/Box/Box.js +3 -2
- package/dist/Components/Base/Typography/Typography.d.ts +2 -1
- package/dist/Components/Base/Typography/Typography.js +1 -1
- package/dist/Components/Base/Typography/Typography.types.d.ts +4 -5
- package/dist/Components/Inputs/Calendar/Calendar.d.ts +2 -1
- package/dist/Components/Inputs/Calendar/Calendar.js +9 -8
- package/dist/Components/Inputs/Calendar/Calendar.parts.js +1 -1
- package/dist/Components/Inputs/Calendar/Calendar.types.d.ts +1 -0
- package/dist/Components/Navigations/GNBList/GNBList.d.ts +1 -1
- package/dist/Components/Navigations/GNBList/GNBList.parts.d.ts +3 -2
- package/dist/Components/Navigations/GNBList/GNBList.parts.js +10 -10
- package/dist/Components/Navigations/GNBList/GNBList.types.d.ts +8 -3
- package/dist/Components/Navigations/Menu/Menu.types.d.ts +53 -0
- package/dist/Components/Navigations/Menu/MenuBase.d.ts +21 -0
- package/dist/Components/Navigations/Menu/MenuBase.js +3 -0
- package/dist/Components/Navigations/Menu/MenuButton.d.ts +40 -0
- package/dist/Components/Navigations/Menu/MenuButton.js +39 -0
- package/dist/Components/Navigations/Menu/MenuLink.d.ts +43 -0
- package/dist/Components/Navigations/Menu/MenuLink.js +42 -0
- package/dist/Components/Navigations/Menu/index.d.ts +3 -1
- package/dist/Components/Navigations/TextLink/TextLink.d.ts +13 -35
- package/dist/Components/Navigations/TextLink/TextLink.js +11 -34
- package/dist/cjs/Components/Base/Layouts/Box/Box.js +3 -2
- package/dist/cjs/Components/Base/Typography/Typography.js +2 -2
- package/dist/cjs/Components/Inputs/Calendar/Calendar.js +7 -7
- package/dist/cjs/Components/Inputs/Calendar/Calendar.parts.js +6 -6
- package/dist/cjs/Components/Navigations/GNBList/GNBList.parts.js +11 -11
- package/dist/cjs/Components/Navigations/Menu/MenuBase.js +3 -0
- package/dist/cjs/Components/Navigations/Menu/MenuButton.js +39 -0
- package/dist/cjs/Components/Navigations/Menu/MenuLink.js +42 -0
- package/dist/cjs/Components/Navigations/TextLink/TextLink.js +11 -34
- package/dist/cjs/index.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +7 -5
- package/scripts/codemods/README.md +178 -0
- package/scripts/codemods/__tests__/__fixtures__/add-as-link/already-has-as.input.tsx +6 -0
- package/scripts/codemods/__tests__/__fixtures__/add-as-link/already-has-as.output.tsx +6 -0
- package/scripts/codemods/__tests__/__fixtures__/add-as-link/basic.input.tsx +9 -0
- package/scripts/codemods/__tests__/__fixtures__/add-as-link/basic.output.tsx +10 -0
- package/scripts/codemods/__tests__/__fixtures__/add-as-link/existing-next-link-import.input.tsx +9 -0
- package/scripts/codemods/__tests__/__fixtures__/add-as-link/existing-next-link-import.output.tsx +9 -0
- package/scripts/codemods/__tests__/__fixtures__/add-as-link/menu-button-fallback.input.tsx +5 -0
- package/scripts/codemods/__tests__/__fixtures__/add-as-link/menu-button-fallback.output.tsx +5 -0
- package/scripts/codemods/__tests__/__fixtures__/add-as-link/unrelated-import.input.tsx +3 -0
- package/scripts/codemods/__tests__/__fixtures__/add-as-link/unrelated-import.output.tsx +3 -0
- package/scripts/codemods/__tests__/__fixtures__/menu-split/basic.input.tsx +8 -0
- package/scripts/codemods/__tests__/__fixtures__/menu-split/basic.output.tsx +8 -0
- package/scripts/codemods/__tests__/__fixtures__/menu-split/conflict.input.tsx +5 -0
- package/scripts/codemods/__tests__/__fixtures__/menu-split/conflict.output.tsx +6 -0
- package/scripts/codemods/__tests__/__fixtures__/menu-split/href-only.input.tsx +3 -0
- package/scripts/codemods/__tests__/__fixtures__/menu-split/href-only.output.tsx +3 -0
- package/scripts/codemods/__tests__/__fixtures__/menu-split/onclick-only.input.tsx +3 -0
- package/scripts/codemods/__tests__/__fixtures__/menu-split/onclick-only.output.tsx +3 -0
- package/scripts/codemods/__tests__/__fixtures__/menu-split/spread-props.input.tsx +3 -0
- package/scripts/codemods/__tests__/__fixtures__/menu-split/spread-props.output.tsx +4 -0
- package/scripts/codemods/__tests__/run-fixtures.cjs +100 -0
- package/scripts/codemods/add-as-link.ts +110 -0
- package/scripts/codemods/menu-split.ts +252 -0
- package/dist/Components/Navigations/Menu/Menu.d.ts +0 -81
- package/dist/Components/Navigations/Menu/Menu.js +0 -62
- package/dist/cjs/Components/Navigations/Menu/Menu.js +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wishket/design-system",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Wishket Design System",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
-
"dist/cjs"
|
|
12
|
+
"dist/cjs",
|
|
13
|
+
"scripts/codemods"
|
|
13
14
|
],
|
|
14
15
|
"exports": {
|
|
15
16
|
".": {
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"build:dts": "tsc --project tsconfig.rollup.json --emitDeclarationOnly",
|
|
39
40
|
"lint": "eslint './**/*.{ts,tsx,js,jsx}' --ignore-path .eslintignore || true",
|
|
40
41
|
"test": "jest --ci --runInBand --coverage --watchAll=false --passWithNoTests --testMatch=\"**/*.spec.@(ts|tsx)\"",
|
|
42
|
+
"test:codemods": "node scripts/codemods/__tests__/run-fixtures.cjs",
|
|
41
43
|
"storybook": "storybook dev -p 6006",
|
|
42
44
|
"build-storybook": "storybook build",
|
|
43
45
|
"prettier:check": "prettier --cache --check .",
|
|
@@ -46,7 +48,6 @@
|
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
48
50
|
"@wishket/yogokit": "^0.2.2",
|
|
49
|
-
"next": "16.0.10",
|
|
50
51
|
"react": "19.2.3",
|
|
51
52
|
"react-dom": "19.2.3",
|
|
52
53
|
"sharp": "^0.34.3",
|
|
@@ -102,6 +103,7 @@
|
|
|
102
103
|
"jest-dom": "^4.0.0",
|
|
103
104
|
"jest-environment-jsdom": "^29.7.0",
|
|
104
105
|
"madge": "^8.0.0",
|
|
106
|
+
"next": "16.0.10",
|
|
105
107
|
"postcss": "^8.5.5",
|
|
106
108
|
"postcss-loader": "^8.1.1",
|
|
107
109
|
"prettier": "^3.6.2",
|
|
@@ -118,8 +120,8 @@
|
|
|
118
120
|
},
|
|
119
121
|
"peerDependencies": {
|
|
120
122
|
"@savvywombat/tailwindcss-grid-areas": "^4.0.0",
|
|
121
|
-
"react": "^
|
|
122
|
-
"react-dom": "^
|
|
123
|
+
"react": "^19",
|
|
124
|
+
"react-dom": "^19"
|
|
123
125
|
},
|
|
124
126
|
"sideEffects": [
|
|
125
127
|
"*.css",
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Codemods
|
|
2
|
+
|
|
3
|
+
`@wishket/design-system` 메이저 마이그레이션을 컨슈머 앱(`wishket-service`, `yozm-service`, `account-service` 등)에 일괄 적용하기 위한 jscodeshift 스크립트 모음입니다.
|
|
4
|
+
|
|
5
|
+
## v3.0 — `add-as-link`
|
|
6
|
+
|
|
7
|
+
v3.0은 `TextLink`, `Menu`, `GNBList.Item`이 더 이상 자동으로 `next/link`를 사용하지 않습니다. 컨슈머는 `as={Link}`를 명시적으로 넘겨야 합니다. 이 codemod는 그 변환을 자동화합니다.
|
|
8
|
+
|
|
9
|
+
### 변환 규칙
|
|
10
|
+
|
|
11
|
+
1. `@wishket/design-system`을 import한 파일에서만 동작.
|
|
12
|
+
2. 다음 호출만 대상으로 함: `<TextLink ... />`, `<Menu ... />`, `<GNBList.Item ... />`.
|
|
13
|
+
3. **`href`가 있는 호출만** 대상 (Menu의 `<button>` 폴백을 보호).
|
|
14
|
+
4. 이미 `as` prop이 있으면 건너뜀.
|
|
15
|
+
5. `next/link` 기본 import가 없으면 자동으로 `import Link from 'next/link';`를 파일 상단에 추가.
|
|
16
|
+
|
|
17
|
+
### 변환 예시
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// === Before ===
|
|
21
|
+
import { TextLink, Menu, GNBList } from '@wishket/design-system';
|
|
22
|
+
|
|
23
|
+
<TextLink href="/about" text="About" />
|
|
24
|
+
<Menu name="Settings" href="/settings" />
|
|
25
|
+
<GNBList.Item href="/products">Products</GNBList.Item>
|
|
26
|
+
|
|
27
|
+
// === After ===
|
|
28
|
+
import Link from 'next/link';
|
|
29
|
+
import { TextLink, Menu, GNBList } from '@wishket/design-system';
|
|
30
|
+
|
|
31
|
+
<TextLink as={Link} href="/about" text="About" />
|
|
32
|
+
<Menu as={Link} name="Settings" href="/settings" />
|
|
33
|
+
<GNBList.Item as={Link} href="/products">Products</GNBList.Item>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 권장 4단계 실행 절차
|
|
37
|
+
|
|
38
|
+
> 시니어 친화적 운영 절차. 각 단계를 순서대로 실행하고 결과를 확인 후 다음으로 넘어가세요.
|
|
39
|
+
|
|
40
|
+
#### Step 1 — 사전 준비
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# jscodeshift 설치 (없는 경우)
|
|
44
|
+
yarn add -D jscodeshift @types/jscodeshift
|
|
45
|
+
|
|
46
|
+
# v3 설치/업그레이드
|
|
47
|
+
yarn up @wishket/design-system@3
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### Step 2 — Dry run (미리 보기)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx jscodeshift \
|
|
54
|
+
-t node_modules/@wishket/design-system/scripts/codemods/add-as-link.ts \
|
|
55
|
+
--extensions=tsx,ts \
|
|
56
|
+
--parser=tsx \
|
|
57
|
+
--dry --print \
|
|
58
|
+
src/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`--dry`로 실제 파일 변경 없이 변환될 코드를 콘솔에 표시. 변환 규모와 후보 호출을 확인.
|
|
62
|
+
|
|
63
|
+
#### Step 3 — 실제 적용 + 포맷 + 검증
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 1) codemod 적용
|
|
67
|
+
npx jscodeshift \
|
|
68
|
+
-t node_modules/@wishket/design-system/scripts/codemods/add-as-link.ts \
|
|
69
|
+
--extensions=tsx,ts \
|
|
70
|
+
--parser=tsx \
|
|
71
|
+
src/
|
|
72
|
+
|
|
73
|
+
# 2) prettier로 출력 품질 보정 (codemod는 원래 스타일을 100% 보존하지 못함)
|
|
74
|
+
yarn prettier:write src/
|
|
75
|
+
|
|
76
|
+
# 3) 타입 체크
|
|
77
|
+
yarn type-check # 또는: npx tsc -p tsconfig.json --noEmit
|
|
78
|
+
|
|
79
|
+
# 4) 빌드 확인
|
|
80
|
+
yarn build # 또는 next build / vite build 등
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Step 4 — 수동 처리 후보 grep
|
|
84
|
+
|
|
85
|
+
codemod가 안전하게 변환하지 못하는 케이스를 후처리:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# spread props 케이스
|
|
89
|
+
grep -rn -E "TextLink \{\.\.\.|Menu \{\.\.\.|GNBList\.Item \{\.\.\." src/
|
|
90
|
+
|
|
91
|
+
# alias import 케이스 (있다면)
|
|
92
|
+
grep -rn "as DSTextLink\|as DSMenu\|as DSGNBList" src/
|
|
93
|
+
|
|
94
|
+
# UrlObject href 패턴 (v3에서 string으로 좁혀짐)
|
|
95
|
+
grep -rn "href={{" src/
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
발견된 케이스는 [`MIGRATION_V2_TO_V3.md`](../../MIGRATION_V2_TO_V3.md) §4의 수동 수정 가이드 참조.
|
|
99
|
+
|
|
100
|
+
> 디자인시스템을 v3로 올린 뒤 `node_modules`에 codemod가 함께 들어옵니다 (`scripts/codemods/`는 `package.json`의 `files` 필드에 포함됨).
|
|
101
|
+
|
|
102
|
+
### 한계와 주의사항
|
|
103
|
+
|
|
104
|
+
- **Spread props는 변환 못 함.** `<TextLink {...linkProps} />`처럼 props가 spread로 들어오는 호출은 자동 변환에서 제외됩니다. 다음 명령으로 후보를 찾아 수동 처리하세요.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
grep -rn "TextLink {\\.\\.\\.\\|Menu {\\.\\.\\.\\|GNBList\\.Item {\\.\\.\\." src/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- **alias import 미지원.** `import { TextLink as DSTextLink } from '@wishket/design-system'`처럼 alias를 쓰는 경우는 잡지 못합니다. 사내 컨벤션이 alias를 안 쓰면 무시 가능.
|
|
111
|
+
- **Menu의 button 폴백.** `href`가 없는 `<Menu>` 호출은 의도적으로 변환되지 않습니다. (폴백으로 `<button>`이 렌더되기 때문에 `as`를 붙이면 안 됨.)
|
|
112
|
+
- **codemod는 1차 자동화입니다.** 변환 후 반드시 사람이 diff를 검토하고 CI를 한 번 돌려보세요.
|
|
113
|
+
|
|
114
|
+
## v3.0 — `menu-split`
|
|
115
|
+
|
|
116
|
+
v3.0에서는 `Menu` 컴포넌트가 `MenuLink`(anchor) / `MenuButton`(button)으로 분리됩니다. 시맨틱 분기를 prop 존재 여부로 추론하던 기존 구조를 제거하고 컴포넌트 단위에서 강제하기 위함입니다.
|
|
117
|
+
|
|
118
|
+
> 이 codemod는 `add-as-link` 이후에 실행하세요. `add-as-link`가 `Menu`에 `as={Link}`를 미리 붙여둔 상태라도 `menu-split`이 안전하게 처리합니다.
|
|
119
|
+
|
|
120
|
+
### 변환 규칙
|
|
121
|
+
|
|
122
|
+
1. `@wishket/design-system`에서 `Menu`가 import된 파일만 대상.
|
|
123
|
+
2. JSX 호출 분석:
|
|
124
|
+
- `href` prop 있음 → `<MenuLink ...>`로 rename + import 정리.
|
|
125
|
+
- `href` 없고 `onClick` 있음 → `<MenuButton ...>`로 rename + import 정리.
|
|
126
|
+
- **둘 다 있음 (모순 케이스)** → 변환하지 않고 위에 `// TODO: codemod could not safely resolve — manually choose MenuLink or MenuButton` 코멘트 삽입.
|
|
127
|
+
- **둘 다 없음** → 변환하지 않고 같은 형식의 TODO 코멘트 삽입.
|
|
128
|
+
- **spread props** (`<Menu {...props} />`) → 변환하지 않고 TODO 코멘트 삽입.
|
|
129
|
+
3. `Menu` import는 사용된 변형으로 교체. 변환 못 한 호출이 남아있으면 `Menu`도 함께 유지.
|
|
130
|
+
4. **alias import** (예: `Menu as M`)는 안전하게 변환할 수 없으므로 파일 상단에 TODO 코멘트만 추가합니다.
|
|
131
|
+
|
|
132
|
+
### 변환 예시
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
// === Before ===
|
|
136
|
+
import { Menu } from '@wishket/design-system';
|
|
137
|
+
|
|
138
|
+
<Menu name="Projects" href="/projects" />
|
|
139
|
+
<Menu name="Settings" onClick={() => openModal()} />
|
|
140
|
+
|
|
141
|
+
// === After ===
|
|
142
|
+
import { MenuLink, MenuButton } from '@wishket/design-system';
|
|
143
|
+
|
|
144
|
+
<MenuLink name="Projects" href="/projects" />
|
|
145
|
+
<MenuButton name="Settings" onClick={() => openModal()} />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 실행 방법
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# 미리 보기
|
|
152
|
+
npx jscodeshift \
|
|
153
|
+
-t node_modules/@wishket/design-system/scripts/codemods/menu-split.ts \
|
|
154
|
+
--extensions=tsx,ts --parser=tsx \
|
|
155
|
+
--dry --print \
|
|
156
|
+
src/
|
|
157
|
+
|
|
158
|
+
# 실제 적용
|
|
159
|
+
npx jscodeshift \
|
|
160
|
+
-t node_modules/@wishket/design-system/scripts/codemods/menu-split.ts \
|
|
161
|
+
--extensions=tsx,ts --parser=tsx \
|
|
162
|
+
src/
|
|
163
|
+
|
|
164
|
+
# 후처리
|
|
165
|
+
yarn prettier:write src/
|
|
166
|
+
yarn type-check
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 한계와 주의사항
|
|
170
|
+
|
|
171
|
+
- **alias import 미지원.** `import { Menu as M } from '@wishket/design-system'`는 안전 변환 불가. TODO 코멘트만 추가.
|
|
172
|
+
- **spread props 미지원.** `<Menu {...props} />`는 prop 존재 여부를 정적으로 결정할 수 없어 변환되지 않습니다. 다음 명령으로 후보를 찾으세요.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
grep -rn "Menu {\\.\\.\\." src/
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- **모순 케이스(`href` + `onClick`).** 기존 `Menu`는 `href`가 있으면 anchor로 렌더되며 `onClick`은 무시됐습니다. codemod는 의도를 알 수 없으므로 변환하지 않고 TODO 코멘트를 답니다 — 컨슈머가 `MenuLink`로 옮길지 `MenuButton`으로 옮길지 직접 결정해야 합니다.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { TextLink, Menu, GNBList } from '@wishket/design-system';
|
|
3
|
+
|
|
4
|
+
export const Page = () => (
|
|
5
|
+
<>
|
|
6
|
+
<TextLink as={Link} href="/about" text="About" />
|
|
7
|
+
<Menu as={Link} name="Settings" href="/settings" />
|
|
8
|
+
<GNBList.Item as={Link} href="/products">Products</GNBList.Item>
|
|
9
|
+
</>
|
|
10
|
+
);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Codemod fixture 검증 스크립트.
|
|
4
|
+
*
|
|
5
|
+
* 각 codemod에 대해 `__fixtures__/<codemod-name>/<case>.input.tsx`를
|
|
6
|
+
* jscodeshift로 변환한 결과를 `<case>.output.tsx`와 비교한다.
|
|
7
|
+
*
|
|
8
|
+
* 사용:
|
|
9
|
+
* node scripts/codemods/__tests__/run-fixtures.js
|
|
10
|
+
*
|
|
11
|
+
* 회귀 방어 목적. CI에서 매 PR 마다 실행하면 codemod 변경 시 회귀를 잡을 수 있다.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const CODEMODS = ['add-as-link', 'menu-split'];
|
|
19
|
+
const ROOT = path.join(__dirname, '..');
|
|
20
|
+
const FIXTURES_ROOT = path.join(__dirname, '__fixtures__');
|
|
21
|
+
|
|
22
|
+
function normalize(s) {
|
|
23
|
+
// Whitespace + 사소한 포맷 차이 흡수
|
|
24
|
+
return s.replace(/\s+/g, ' ').replace(/\s*([{}<>=,;])\s*/g, '$1').trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runCodemod(codemodPath, inputPath) {
|
|
28
|
+
// jscodeshift를 stdout으로 출력
|
|
29
|
+
const out = execSync(
|
|
30
|
+
`npx jscodeshift -t "${codemodPath}" --parser=tsx --dry --print --silent --stdin --no-babel < "${inputPath}"`,
|
|
31
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
|
|
32
|
+
).trim();
|
|
33
|
+
|
|
34
|
+
// --print는 변환된 코드를 출력하지만 헤더(파일 경로 등)도 같이 나옴.
|
|
35
|
+
// 가장 단순한 방법: tmp 파일에 input을 복사 후 변환 결과를 stdout으로 읽기
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let passed = 0;
|
|
40
|
+
let failed = 0;
|
|
41
|
+
const failures = [];
|
|
42
|
+
|
|
43
|
+
for (const codemod of CODEMODS) {
|
|
44
|
+
const codemodPath = path.join(ROOT, `${codemod}.ts`);
|
|
45
|
+
const fixturesDir = path.join(FIXTURES_ROOT, codemod);
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(fixturesDir)) {
|
|
48
|
+
console.log(`(skip) no fixtures for ${codemod}`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const cases = fs
|
|
53
|
+
.readdirSync(fixturesDir)
|
|
54
|
+
.filter(f => f.endsWith('.input.tsx'))
|
|
55
|
+
.map(f => f.replace('.input.tsx', ''));
|
|
56
|
+
|
|
57
|
+
for (const c of cases) {
|
|
58
|
+
const inputPath = path.join(fixturesDir, `${c}.input.tsx`);
|
|
59
|
+
const outputPath = path.join(fixturesDir, `${c}.output.tsx`);
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(outputPath)) {
|
|
62
|
+
failed++;
|
|
63
|
+
failures.push(`${codemod}/${c}: missing ${c}.output.tsx`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Input을 임시 파일로 복사한 뒤 jscodeshift로 in-place 변환, 결과 읽기
|
|
68
|
+
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'codemod-test-'));
|
|
69
|
+
const tmpFile = path.join(tmpDir, `${c}.tsx`);
|
|
70
|
+
fs.copyFileSync(inputPath, tmpFile);
|
|
71
|
+
try {
|
|
72
|
+
execSync(
|
|
73
|
+
`npx jscodeshift -t "${codemodPath}" --parser=tsx --extensions=tsx --no-babel "${tmpFile}"`,
|
|
74
|
+
{ stdio: 'pipe' },
|
|
75
|
+
);
|
|
76
|
+
const actual = fs.readFileSync(tmpFile, 'utf8');
|
|
77
|
+
const expected = fs.readFileSync(outputPath, 'utf8');
|
|
78
|
+
|
|
79
|
+
if (normalize(actual) === normalize(expected)) {
|
|
80
|
+
passed++;
|
|
81
|
+
console.log(` ✓ ${codemod}/${c}`);
|
|
82
|
+
} else {
|
|
83
|
+
failed++;
|
|
84
|
+
failures.push(
|
|
85
|
+
`${codemod}/${c}:\n--- expected ---\n${expected}\n--- actual ---\n${actual}`,
|
|
86
|
+
);
|
|
87
|
+
console.log(` ✗ ${codemod}/${c}`);
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
96
|
+
if (failed > 0) {
|
|
97
|
+
console.log('\n--- failures ---');
|
|
98
|
+
failures.forEach(f => console.log(f + '\n'));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
API,
|
|
3
|
+
ASTPath,
|
|
4
|
+
Collection,
|
|
5
|
+
FileInfo,
|
|
6
|
+
ImportDeclaration,
|
|
7
|
+
JSCodeshift,
|
|
8
|
+
JSXAttribute,
|
|
9
|
+
JSXMemberExpression,
|
|
10
|
+
JSXOpeningElement,
|
|
11
|
+
} from 'jscodeshift';
|
|
12
|
+
|
|
13
|
+
const TARGET_COMPONENTS = new Set([
|
|
14
|
+
'TextLink',
|
|
15
|
+
'Menu',
|
|
16
|
+
'GNBList.Item',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const DESIGN_SYSTEM_PACKAGE = '@wishket/design-system';
|
|
20
|
+
const NEXT_LINK_PACKAGE = 'next/link';
|
|
21
|
+
const NEXT_LINK_LOCAL_NAME = 'Link';
|
|
22
|
+
|
|
23
|
+
export default function transformer(file: FileInfo, api: API): string {
|
|
24
|
+
const j: JSCodeshift = api.jscodeshift;
|
|
25
|
+
const root = j(file.source);
|
|
26
|
+
|
|
27
|
+
const dsImport: Collection<ImportDeclaration> = root.find(
|
|
28
|
+
j.ImportDeclaration,
|
|
29
|
+
{
|
|
30
|
+
source: { value: DESIGN_SYSTEM_PACKAGE },
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
if (dsImport.size() === 0) return file.source;
|
|
34
|
+
|
|
35
|
+
let didChange = false;
|
|
36
|
+
|
|
37
|
+
root
|
|
38
|
+
.find(j.JSXOpeningElement)
|
|
39
|
+
.forEach((path: ASTPath<JSXOpeningElement>) => {
|
|
40
|
+
const name = getJSXName(path.node);
|
|
41
|
+
if (!name || !TARGET_COMPONENTS.has(name)) return;
|
|
42
|
+
|
|
43
|
+
const attrs = path.node.attributes ?? [];
|
|
44
|
+
|
|
45
|
+
const hasAs = attrs.some(
|
|
46
|
+
(attr): attr is JSXAttribute =>
|
|
47
|
+
attr.type === 'JSXAttribute' &&
|
|
48
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
49
|
+
attr.name.name === 'as',
|
|
50
|
+
);
|
|
51
|
+
if (hasAs) return;
|
|
52
|
+
|
|
53
|
+
const hasHref = attrs.some(
|
|
54
|
+
(attr): attr is JSXAttribute =>
|
|
55
|
+
attr.type === 'JSXAttribute' &&
|
|
56
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
57
|
+
attr.name.name === 'href',
|
|
58
|
+
);
|
|
59
|
+
if (!hasHref) return;
|
|
60
|
+
|
|
61
|
+
const asAttr = j.jsxAttribute(
|
|
62
|
+
j.jsxIdentifier('as'),
|
|
63
|
+
j.jsxExpressionContainer(j.identifier(NEXT_LINK_LOCAL_NAME)),
|
|
64
|
+
);
|
|
65
|
+
path.node.attributes = [asAttr, ...attrs];
|
|
66
|
+
didChange = true;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!didChange) return file.source;
|
|
70
|
+
|
|
71
|
+
const hasNextLinkImport =
|
|
72
|
+
root
|
|
73
|
+
.find(j.ImportDeclaration, {
|
|
74
|
+
source: { value: NEXT_LINK_PACKAGE },
|
|
75
|
+
})
|
|
76
|
+
.size() > 0;
|
|
77
|
+
|
|
78
|
+
if (!hasNextLinkImport) {
|
|
79
|
+
const newImport = j.importDeclaration(
|
|
80
|
+
[j.importDefaultSpecifier(j.identifier(NEXT_LINK_LOCAL_NAME))],
|
|
81
|
+
j.literal(NEXT_LINK_PACKAGE),
|
|
82
|
+
);
|
|
83
|
+
root.get().node.program.body.unshift(newImport);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return root.toSource({ quote: 'single' });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getJSXName(node: JSXOpeningElement): string | null {
|
|
90
|
+
const { name } = node;
|
|
91
|
+
if (name.type === 'JSXIdentifier') return name.name;
|
|
92
|
+
if (name.type === 'JSXMemberExpression') {
|
|
93
|
+
return memberExpressionToString(name);
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function memberExpressionToString(
|
|
99
|
+
expr: JSXMemberExpression,
|
|
100
|
+
): string | null {
|
|
101
|
+
const right = expr.property.name;
|
|
102
|
+
if (expr.object.type === 'JSXIdentifier') {
|
|
103
|
+
return `${expr.object.name}.${right}`;
|
|
104
|
+
}
|
|
105
|
+
if (expr.object.type === 'JSXMemberExpression') {
|
|
106
|
+
const left = memberExpressionToString(expr.object);
|
|
107
|
+
return left ? `${left}.${right}` : null;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|