vuetify-codemods 1.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/.nvmrc +1 -0
- package/LICENSE +557 -0
- package/README.md +17 -0
- package/dist/main.js +96181 -0
- package/eslint.config.js +9 -0
- package/package.json +36 -0
- package/patches/vue-metamorph.patch +38 -0
- package/pnpm-workspace.yaml +5 -0
- package/src/helpers.ts +259 -0
- package/src/main.ts +83 -0
- package/src/plugins/v4/combobox-item-slot.spec.ts +103 -0
- package/src/plugins/v4/combobox-item-slot.ts +100 -0
- package/src/plugins/v4/elevation.spec.ts +23 -0
- package/src/plugins/v4/elevation.ts +97 -0
- package/src/plugins/v4/form-slot-refs.spec.ts +51 -0
- package/src/plugins/v4/form-slot-refs.ts +22 -0
- package/src/plugins/v4/grid.spec.ts +282 -0
- package/src/plugins/v4/grid.ts +103 -0
- package/src/plugins/v4/index.ts +16 -0
- package/src/plugins/v4/snackbar-multiline.spec.ts +19 -0
- package/src/plugins/v4/snackbar-multiline.ts +40 -0
- package/src/plugins/v4/snackbar-queue-slot.spec.ts +53 -0
- package/src/plugins/v4/snackbar-queue-slot.ts +25 -0
- package/src/plugins/v4/typography.spec.ts +31 -0
- package/src/plugins/v4/typography.ts +40 -0
- package/tsconfig.eslint.json +8 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +25 -0
package/eslint.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vuetify-codemods",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"bin": {
|
|
6
|
+
"vuetify-codemods": "dist/main.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc && vite build && chmod +x dist/main.js",
|
|
10
|
+
"lint": "eslint . --ext .ts",
|
|
11
|
+
"test": "vitest"
|
|
12
|
+
},
|
|
13
|
+
"license": "SSPL",
|
|
14
|
+
"homepage": "https://github.com/vuetifyjs/vuetify-codemods",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/vuetifyjs/vuetify-codemods.git"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@inquirer/prompts": "^8.3.0",
|
|
21
|
+
"esprima": "^4.0.1",
|
|
22
|
+
"lodash-es": "^4.17.23",
|
|
23
|
+
"vue-eslint-parser": "^10.4.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^24.10.13",
|
|
27
|
+
"ast-types-x": "^1.18.0",
|
|
28
|
+
"eslint": "^9.39.2",
|
|
29
|
+
"eslint-config-vuetify": "4.3.5-beta.1",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"vite": "^7.3.1",
|
|
32
|
+
"vitest": "^4.0.18",
|
|
33
|
+
"vue-metamorph": "^3.2.0"
|
|
34
|
+
},
|
|
35
|
+
"packageManager": "pnpm@10.30.0"
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
diff --git a/dist/main.js b/dist/main.js
|
|
2
|
+
index 0c1f4080b23e8ab8f7f152ed29e8fe2254652883..d1f0d9403b904fb25b1c93b6fbcea0988e668c07 100644
|
|
3
|
+
--- a/dist/main.js
|
|
4
|
+
+++ b/dist/main.js
|
|
5
|
+
@@ -103,14 +103,18 @@ function stringifyVDirectiveKey(node) {
|
|
6
|
+
return str;
|
|
7
|
+
}
|
|
8
|
+
function stringifyVLiteral(node) {
|
|
9
|
+
- return node.value;
|
|
10
|
+
+ return `"${node.value}"`
|
|
11
|
+
}
|
|
12
|
+
function stringifyVAttribute(node) {
|
|
13
|
+
- let str = node.directive ? stringifyVDirectiveKey(node.key) : node.key.rawName;
|
|
14
|
+
+ let str = node.directive ? stringifyVDirectiveKey(node.key) : node.key.rawName
|
|
15
|
+
if (node.value) {
|
|
16
|
+
- str += `="${stringify(node.value)}"`;
|
|
17
|
+
+ if (node.value.type === 'VLiteral') {
|
|
18
|
+
+ str += `="${node.value.value}"`
|
|
19
|
+
+ } else {
|
|
20
|
+
+ str += `="${stringify(node.value)}"`
|
|
21
|
+
+ }
|
|
22
|
+
}
|
|
23
|
+
- return str;
|
|
24
|
+
+ return str
|
|
25
|
+
}
|
|
26
|
+
function stringifyVStartTag(node, isVoidElement = false) {
|
|
27
|
+
let str = "";
|
|
28
|
+
@@ -935,10 +939,6 @@ function transformVueFile(code, filename, codemods, opts) {
|
|
29
|
+
if (path.at(-2) === "key") {
|
|
30
|
+
path.pop();
|
|
31
|
+
}
|
|
32
|
+
- if (path.at(-1) === "expression") {
|
|
33
|
+
- path.pop();
|
|
34
|
+
- path.pop();
|
|
35
|
+
- }
|
|
36
|
+
if (path.at(-1) === "body") {
|
|
37
|
+
path.pop();
|
|
38
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { namedTypes } from 'ast-types-x'
|
|
2
|
+
import type { AST as VueAST } from 'vue-eslint-parser'
|
|
3
|
+
import type { AST, CodemodPluginContext } from 'vue-metamorph'
|
|
4
|
+
import { astHelpers } from 'vue-metamorph'
|
|
5
|
+
|
|
6
|
+
const camelizeRE = /-(\w)/g
|
|
7
|
+
export function camelize (str: string): string {
|
|
8
|
+
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const classifyRE = /(?:^|[-_])(\w)/g
|
|
12
|
+
export function classify (str: string): string {
|
|
13
|
+
return str
|
|
14
|
+
.replace(classifyRE, c => c.toUpperCase())
|
|
15
|
+
.replace(/[-_]/g, '')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function findSlotNodes (
|
|
19
|
+
ast: AST.VDocumentFragment,
|
|
20
|
+
components: string[] | null,
|
|
21
|
+
slots: string[] | null,
|
|
22
|
+
) {
|
|
23
|
+
return astHelpers.findAll(ast, {
|
|
24
|
+
type: 'VAttribute',
|
|
25
|
+
directive: true,
|
|
26
|
+
key: {
|
|
27
|
+
type: 'VDirectiveKey',
|
|
28
|
+
name: {
|
|
29
|
+
type: 'VIdentifier',
|
|
30
|
+
name: 'slot',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}).filter(node => {
|
|
34
|
+
if (components && !(
|
|
35
|
+
node.parent.type === 'VStartTag'
|
|
36
|
+
&& (
|
|
37
|
+
(
|
|
38
|
+
node.parent.parent.name === 'template'
|
|
39
|
+
&& node.parent.parent.parent.type === 'VElement'
|
|
40
|
+
&& components.includes(classify(node.parent.parent.parent.rawName))
|
|
41
|
+
)
|
|
42
|
+
|| components.includes(classify(node.parent.parent.rawName))
|
|
43
|
+
)
|
|
44
|
+
)) return false
|
|
45
|
+
if (slots && !(
|
|
46
|
+
node.key.type === 'VDirectiveKey'
|
|
47
|
+
&& (
|
|
48
|
+
(!node.key.argument && slots.includes('default'))
|
|
49
|
+
|| (
|
|
50
|
+
node.key.argument?.type === 'VIdentifier'
|
|
51
|
+
&& slots.includes(node.key.argument.name)
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
)) return false
|
|
55
|
+
return true
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type Ref = { prop: namedTypes.Property | null, reference: VueAST.ESLintIdentifier }
|
|
60
|
+
export function findSlotPropReferences (
|
|
61
|
+
slot: AST.VAttribute | AST.VDirective,
|
|
62
|
+
props: string[] | null,
|
|
63
|
+
) {
|
|
64
|
+
const results: Ref[] = []
|
|
65
|
+
if (
|
|
66
|
+
!slot.value
|
|
67
|
+
|| slot.value.type !== 'VExpressionContainer'
|
|
68
|
+
|| slot.value.expression?.type !== 'VSlotScopeExpression'
|
|
69
|
+
) return results
|
|
70
|
+
const slotExpression = slot.value.expression
|
|
71
|
+
// vue-metamorph has its own incomplete AST types for some
|
|
72
|
+
// reason instead of re-exporting them from vue-eslint-parser
|
|
73
|
+
const templateElement = slot.parent.parent as unknown as VueAST.VElement
|
|
74
|
+
|
|
75
|
+
const searchProps = new Set(props)
|
|
76
|
+
|
|
77
|
+
if (slotExpression.params.length === 1 && slotExpression.params[0]!.type === 'ObjectPattern') {
|
|
78
|
+
// Destructured props (v-slot="{ prop }")
|
|
79
|
+
for (const prop of slotExpression.params[0].properties) {
|
|
80
|
+
if (
|
|
81
|
+
prop.type === 'Property'
|
|
82
|
+
&& prop.key.type === 'Identifier'
|
|
83
|
+
&& searchProps.has(prop.key.name)
|
|
84
|
+
) {
|
|
85
|
+
const variable = templateElement.variables.find(v =>
|
|
86
|
+
prop.value.type === 'Identifier'
|
|
87
|
+
&& v.id.name === prop.value.name,
|
|
88
|
+
)
|
|
89
|
+
if (!variable) continue
|
|
90
|
+
for (const reference of variable.references) {
|
|
91
|
+
results.push({
|
|
92
|
+
prop,
|
|
93
|
+
reference: reference.id,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} else if (slotExpression.params.length === 1 && slotExpression.params[0]!.type === 'Identifier') {
|
|
99
|
+
// Non-destructured (v-slot="data")
|
|
100
|
+
const paramName = slotExpression.params[0].name
|
|
101
|
+
const paramVariable = templateElement.variables.find(v => v.id.name === paramName)
|
|
102
|
+
if (paramVariable) {
|
|
103
|
+
for (const reference of paramVariable.references) {
|
|
104
|
+
const memberExpr = reference.id.parent
|
|
105
|
+
if (
|
|
106
|
+
memberExpr?.type === 'MemberExpression'
|
|
107
|
+
&& memberExpr.property.type === 'Identifier'
|
|
108
|
+
&& searchProps.has(memberExpr.property.name)
|
|
109
|
+
) {
|
|
110
|
+
results.push({
|
|
111
|
+
prop: null,
|
|
112
|
+
reference: memberExpr.property,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return results
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// A map of components and props that should be treated the same as class
|
|
122
|
+
const classProps = new Map<string, string | string[]>([
|
|
123
|
+
['VAppBarNavIcon', 'selectedClass'],
|
|
124
|
+
['VBottomNavigation', 'selectedClass'],
|
|
125
|
+
['VBottomSheet', 'contentClass'],
|
|
126
|
+
['VBreadcrumbs', 'activeClass'],
|
|
127
|
+
['VBreadcrumbsItem', 'activeClass'],
|
|
128
|
+
['VBtn', 'selectedClass'],
|
|
129
|
+
['VBtnToggle', 'selectedClass'],
|
|
130
|
+
['VCarousel', 'selectedClass'],
|
|
131
|
+
['VCarouselItem', ['selectedClass', 'contentClass']],
|
|
132
|
+
['VChip', ['activeClass', 'selectedClass']],
|
|
133
|
+
['VChipGroup', ['contentClass', 'selectedClass']],
|
|
134
|
+
['VDialog', 'contentClass'],
|
|
135
|
+
['VExpansionPanel', 'selectedClass'],
|
|
136
|
+
['VExpansionPanels', 'selectedClass'],
|
|
137
|
+
['VFab', 'selectedClass'],
|
|
138
|
+
['VFileUploadItem', 'activeClass'],
|
|
139
|
+
['VImg', 'contentClass'],
|
|
140
|
+
['VItem', 'selectedClass'],
|
|
141
|
+
['VItemGroup', 'selectedClass'],
|
|
142
|
+
['VList', 'activeClass'],
|
|
143
|
+
['VListItem', 'activeClass'],
|
|
144
|
+
['VMenu', 'contentClass'],
|
|
145
|
+
['VOverlay', 'contentClass'],
|
|
146
|
+
['VResponsive', 'contentClass'],
|
|
147
|
+
['VSlideGroup', ['contentClass', 'selectedClass']],
|
|
148
|
+
['VSlideGroupItem', 'selectedClass'],
|
|
149
|
+
['VSnackbar', 'contentClass'],
|
|
150
|
+
['VSnackbarQueue', 'contentClass'],
|
|
151
|
+
['VSpeedDial', 'contentClass'],
|
|
152
|
+
['VStepper', 'selectedClass'],
|
|
153
|
+
['VStepperItem', 'selectedClass'],
|
|
154
|
+
['VStepperVertical', 'selectedClass'],
|
|
155
|
+
['VStepperVerticalItem', 'selectedClass'],
|
|
156
|
+
['VStepperWindow', 'selectedClass'],
|
|
157
|
+
['VStepperWindowItem', 'selectedClass'],
|
|
158
|
+
['VTab', 'selectedClass'],
|
|
159
|
+
['VTabs', ['selectedClass', 'contentClass']],
|
|
160
|
+
['VTabsWindow', 'selectedClass'],
|
|
161
|
+
['VTabsWindowItem', 'selectedClass'],
|
|
162
|
+
['VTooltip', 'contentClass'],
|
|
163
|
+
['VTreeview', 'activeClass'],
|
|
164
|
+
['VTreeviewItem', 'activeClass'],
|
|
165
|
+
['VWindow', 'selectedClass'],
|
|
166
|
+
['VWindowItem', 'selectedClass'],
|
|
167
|
+
])
|
|
168
|
+
|
|
169
|
+
export function findClassNodes (ast: AST.VDocumentFragment, utils: CodemodPluginContext['utils'], match: string[]) {
|
|
170
|
+
const matchingRegexp = new RegExp(String.raw`(^|\s)(?:${match.join('|')})(?=$|\s)`)
|
|
171
|
+
|
|
172
|
+
const results: (AST.VLiteral | namedTypes.Literal | namedTypes.Identifier)[] = []
|
|
173
|
+
|
|
174
|
+
const attrs = astHelpers.findAll(ast, { type: 'VAttribute' })
|
|
175
|
+
for (const node of attrs) {
|
|
176
|
+
let attributeName: string
|
|
177
|
+
if (node.key.type === 'VIdentifier') {
|
|
178
|
+
attributeName = camelize(node.key.name)
|
|
179
|
+
} else if (
|
|
180
|
+
node.key.type === 'VDirectiveKey'
|
|
181
|
+
&& node.key.name.name === 'bind'
|
|
182
|
+
&& node.key.argument?.type === 'VIdentifier'
|
|
183
|
+
) {
|
|
184
|
+
attributeName = camelize(node.key.argument.name)
|
|
185
|
+
} else {
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const elementName = classify(node.parent.parent.rawName)
|
|
190
|
+
|
|
191
|
+
const allowed = new Set<string>(['class'])
|
|
192
|
+
if (elementName && classProps.has(elementName)) {
|
|
193
|
+
const prop = classProps.get(elementName)
|
|
194
|
+
if (Array.isArray(prop)) for (const p of prop) allowed.add(camelize(p))
|
|
195
|
+
else if (prop) allowed.add(camelize(prop))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!allowed.has(attributeName)) continue
|
|
199
|
+
|
|
200
|
+
if (!node.value) continue
|
|
201
|
+
|
|
202
|
+
utils.traverseTemplateAST(node.value, {
|
|
203
|
+
enterNode (node) {
|
|
204
|
+
if (
|
|
205
|
+
(node.type === 'VLiteral' || node.type === 'Literal')
|
|
206
|
+
&& typeof node.value === 'string'
|
|
207
|
+
&& matchingRegexp.test(node.value)
|
|
208
|
+
) {
|
|
209
|
+
results.push(node)
|
|
210
|
+
} else if (
|
|
211
|
+
node.type === 'Property'
|
|
212
|
+
&& (node.key.type === 'Literal'
|
|
213
|
+
|| (node.key.type === 'Identifier' && !node.computed)
|
|
214
|
+
)
|
|
215
|
+
) {
|
|
216
|
+
results.push(node.key)
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return results
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function removeDotMember (ref: VueAST.ESLintIdentifier, name: string) {
|
|
226
|
+
let current: VueAST.Node = ref
|
|
227
|
+
let count = 0
|
|
228
|
+
|
|
229
|
+
while (current?.parent) {
|
|
230
|
+
const parent: VueAST.Node = current.parent
|
|
231
|
+
if (parent.type !== 'MemberExpression') break
|
|
232
|
+
|
|
233
|
+
if (
|
|
234
|
+
parent.property.type === 'Identifier'
|
|
235
|
+
&& parent.property.name === name
|
|
236
|
+
&& !parent.computed
|
|
237
|
+
&& parent.parent
|
|
238
|
+
) {
|
|
239
|
+
if (replaceChildNode(parent.parent, parent, current)) {
|
|
240
|
+
count++
|
|
241
|
+
}
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
current = parent
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return count
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function replaceChildNode (parent: VueAST.Node, oldChild: VueAST.Node, newChild: VueAST.Node) {
|
|
252
|
+
for (const key of Object.keys(parent)) {
|
|
253
|
+
if ((parent as any)[key] === oldChild) {
|
|
254
|
+
(parent as any)[key] = newChild
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return false
|
|
259
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import { checkbox, confirm } from '@inquirer/prompts'
|
|
4
|
+
import { createVueMetamorphCli } from 'vue-metamorph'
|
|
5
|
+
import { vuetify4 } from './plugins/v4'
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
try {
|
|
9
|
+
execSync('git diff --quiet', { stdio: 'ignore' })
|
|
10
|
+
} catch {
|
|
11
|
+
const cont = await confirm({
|
|
12
|
+
message: 'Unstaged changes detected, do you want to continue?',
|
|
13
|
+
})
|
|
14
|
+
if (!cont) process.exit()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const choices = await checkbox({
|
|
18
|
+
message: 'Select codemods to apply',
|
|
19
|
+
required: true,
|
|
20
|
+
choices: [
|
|
21
|
+
{
|
|
22
|
+
value: 'vuetify-4-combobox-item-slot',
|
|
23
|
+
name: 'Combobox item slot',
|
|
24
|
+
description: 'https://vuetifyjs.com/en/getting-started/upgrade-guide/#vselect-vcombobox-vautocomplete',
|
|
25
|
+
checked: true,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
value: 'vuetify-4-elevation',
|
|
29
|
+
name: 'Elevation',
|
|
30
|
+
description: 'https://vuetifyjs.com/en/getting-started/upgrade-guide/#elevation',
|
|
31
|
+
checked: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
value: 'vuetify-4-form-slot-refs',
|
|
35
|
+
name: 'Form slot refs',
|
|
36
|
+
description: 'https://vuetifyjs.com/en/getting-started/upgrade-guide/#vform',
|
|
37
|
+
checked: true,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
value: 'vuetify-4-grid',
|
|
41
|
+
name: 'Grid',
|
|
42
|
+
description: 'https://vuetifyjs.com/en/getting-started/upgrade-guide/#grid-system-vrow-and-vcol',
|
|
43
|
+
checked: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
value: 'vuetify-4-snackbar-multiline',
|
|
47
|
+
name: 'Snackbar multi-line',
|
|
48
|
+
description: 'https://vuetifyjs.com/en/getting-started/upgrade-guide/#vsnackbar',
|
|
49
|
+
checked: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
value: 'vuetify-4-snackbar-queue-slot',
|
|
53
|
+
name: 'SnackbarQueue default slot',
|
|
54
|
+
description: 'https://vuetifyjs.com/en/getting-started/upgrade-guide/#vsnackbarqueue',
|
|
55
|
+
checked: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: 'vuetify-4-typography',
|
|
59
|
+
name: 'Typography',
|
|
60
|
+
description: 'https://vuetifyjs.com/en/getting-started/upgrade-guide/#typography',
|
|
61
|
+
checked: true,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const cli = createVueMetamorphCli({
|
|
67
|
+
plugins: [
|
|
68
|
+
vuetify4(),
|
|
69
|
+
].flat().filter(plugin => {
|
|
70
|
+
return choices.includes(plugin.name)
|
|
71
|
+
}),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
process.on('SIGQUIT', cli.abort)
|
|
75
|
+
process.on('SIGTERM', cli.abort)
|
|
76
|
+
process.on('SIGINT', cli.abort)
|
|
77
|
+
|
|
78
|
+
await cli.run()
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!(error instanceof Error && error.name === 'ExitPromptError')) {
|
|
81
|
+
throw error
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { expect, it } from 'vitest'
|
|
2
|
+
import { transform } from 'vue-metamorph'
|
|
3
|
+
import { v4ComboboxItemSlotPlugin } from './combobox-item-slot'
|
|
4
|
+
|
|
5
|
+
it('removes .raw if all usages are raw', () => {
|
|
6
|
+
const input = `
|
|
7
|
+
<template>
|
|
8
|
+
<VSelect item-title="name">
|
|
9
|
+
<template #item="{ item, props }">
|
|
10
|
+
<VListItem v-bind="props" :title="item.raw.name" />
|
|
11
|
+
</template>
|
|
12
|
+
</VSelect>
|
|
13
|
+
</template>
|
|
14
|
+
`
|
|
15
|
+
|
|
16
|
+
expect(transform(input, 'file.vue', [v4ComboboxItemSlotPlugin]).code).toMatchInlineSnapshot(`
|
|
17
|
+
"
|
|
18
|
+
<template>
|
|
19
|
+
<VSelect item-title="name">
|
|
20
|
+
<template #item="{ item, props }">
|
|
21
|
+
<VListItem v-bind="props" :title="item.name" />
|
|
22
|
+
</template>
|
|
23
|
+
</VSelect>
|
|
24
|
+
</template>
|
|
25
|
+
"
|
|
26
|
+
`)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('aliases item to internalItem', () => {
|
|
30
|
+
const input = `
|
|
31
|
+
<template>
|
|
32
|
+
<VSelect item-title="name">
|
|
33
|
+
<template #item="{ item, props }">
|
|
34
|
+
<VListItem v-bind="props" :title="item.title" />
|
|
35
|
+
<VListItem v-bind="props" :title="item.raw.name" />
|
|
36
|
+
</template>
|
|
37
|
+
</VSelect>
|
|
38
|
+
</template>
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
expect(transform(input, 'file.vue', [v4ComboboxItemSlotPlugin]).code).toMatchInlineSnapshot(`
|
|
42
|
+
"
|
|
43
|
+
<template>
|
|
44
|
+
<VSelect item-title="name">
|
|
45
|
+
<template #item="{ internalItem: item, props }">
|
|
46
|
+
<VListItem v-bind="props" :title="item.title" />
|
|
47
|
+
<VListItem v-bind="props" :title="item.raw.name" />
|
|
48
|
+
</template>
|
|
49
|
+
</VSelect>
|
|
50
|
+
</template>
|
|
51
|
+
"
|
|
52
|
+
`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('handles already aliased values', () => {
|
|
56
|
+
const input = `
|
|
57
|
+
<template>
|
|
58
|
+
<VSelect item-title="name">
|
|
59
|
+
<template #item="{ item: selectItem, props }">
|
|
60
|
+
<VListItem v-bind="props" :title="selectItem.title" />
|
|
61
|
+
</template>
|
|
62
|
+
</VSelect>
|
|
63
|
+
</template>
|
|
64
|
+
`
|
|
65
|
+
|
|
66
|
+
expect(transform(input, 'file.vue', [v4ComboboxItemSlotPlugin]).code).toMatchInlineSnapshot(`
|
|
67
|
+
"
|
|
68
|
+
<template>
|
|
69
|
+
<VSelect item-title="name">
|
|
70
|
+
<template #item="{ internalItem: selectItem, props }">
|
|
71
|
+
<VListItem v-bind="props" :title="selectItem.title" />
|
|
72
|
+
</template>
|
|
73
|
+
</VSelect>
|
|
74
|
+
</template>
|
|
75
|
+
"
|
|
76
|
+
`)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('handles non-destructured values', () => {
|
|
80
|
+
const input = `
|
|
81
|
+
<template>
|
|
82
|
+
<VSelect item-title="name">
|
|
83
|
+
<template #item="data">
|
|
84
|
+
<VListItem v-bind="data.props" :title="data.item.title" />
|
|
85
|
+
<VListItem v-bind="data.props" :title="data.item.raw.name" />
|
|
86
|
+
</template>
|
|
87
|
+
</VSelect>
|
|
88
|
+
</template>
|
|
89
|
+
`
|
|
90
|
+
|
|
91
|
+
expect(transform(input, 'file.vue', [v4ComboboxItemSlotPlugin]).code).toMatchInlineSnapshot(`
|
|
92
|
+
"
|
|
93
|
+
<template>
|
|
94
|
+
<VSelect item-title="name">
|
|
95
|
+
<template #item="data">
|
|
96
|
+
<VListItem v-bind="data.props" :title="data.internalItem.title" />
|
|
97
|
+
<VListItem v-bind="data.props" :title="data.item.name" />
|
|
98
|
+
</template>
|
|
99
|
+
</VSelect>
|
|
100
|
+
</template>
|
|
101
|
+
"
|
|
102
|
+
`)
|
|
103
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { CodemodPlugin } from 'vue-metamorph'
|
|
2
|
+
import type { Ref } from '../../helpers'
|
|
3
|
+
import { findSlotNodes, findSlotPropReferences, removeDotMember } from '../../helpers'
|
|
4
|
+
|
|
5
|
+
export const v4ComboboxItemSlotPlugin: CodemodPlugin = {
|
|
6
|
+
type: 'codemod',
|
|
7
|
+
name: 'vuetify-4-combobox-item-slot',
|
|
8
|
+
transform ({ sfcAST }) {
|
|
9
|
+
if (!sfcAST) return 0
|
|
10
|
+
let count = 0
|
|
11
|
+
|
|
12
|
+
const slotNodes = findSlotNodes(
|
|
13
|
+
sfcAST,
|
|
14
|
+
['VSelect', 'VAutocomplete', 'VCombobox'],
|
|
15
|
+
['item', 'chip', 'selection'],
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
for (const node of slotNodes) {
|
|
19
|
+
const refs = findSlotPropReferences(node, ['item'])
|
|
20
|
+
|
|
21
|
+
const destructuredRefs = new Map<string, Ref[]>()
|
|
22
|
+
const variableRefs: Ref[] = []
|
|
23
|
+
|
|
24
|
+
for (const ref of refs) {
|
|
25
|
+
if (ref.prop) {
|
|
26
|
+
if (ref.prop.key.type !== 'Identifier') continue
|
|
27
|
+
const arr = destructuredRefs.get(ref.prop.key.name) ?? []
|
|
28
|
+
arr.push(ref)
|
|
29
|
+
destructuredRefs.set(ref.prop.key.name, arr)
|
|
30
|
+
} else {
|
|
31
|
+
variableRefs.push(ref)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Process destructured refs (e.g. v-slot="{ item }")
|
|
36
|
+
const destructuredItemRefs = destructuredRefs.get('item') ?? []
|
|
37
|
+
if (destructuredItemRefs.length > 0) {
|
|
38
|
+
let hasRaw = false
|
|
39
|
+
let hasNonRaw = false
|
|
40
|
+
for (const ref of destructuredItemRefs) {
|
|
41
|
+
const parent = ref.reference.parent
|
|
42
|
+
if (parent?.type === 'MemberExpression' && parent.property.type === 'Identifier') {
|
|
43
|
+
if (parent.property.name === 'raw') hasRaw = true
|
|
44
|
+
else hasNonRaw = true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (hasNonRaw) {
|
|
49
|
+
// Alias item -> internalItem in the object pattern
|
|
50
|
+
if (
|
|
51
|
+
node.value?.type === 'VExpressionContainer'
|
|
52
|
+
&& node.value.expression?.type === 'VSlotScopeExpression'
|
|
53
|
+
) {
|
|
54
|
+
const obj = node.value.expression.params?.[0]
|
|
55
|
+
if (obj?.type === 'ObjectPattern') {
|
|
56
|
+
for (const prop of obj.properties) {
|
|
57
|
+
if (
|
|
58
|
+
prop.type === 'Property'
|
|
59
|
+
&& prop.key.type === 'Identifier'
|
|
60
|
+
&& prop.key.name === 'item'
|
|
61
|
+
) {
|
|
62
|
+
prop.key.name = 'internalItem'
|
|
63
|
+
prop.shorthand = false
|
|
64
|
+
count++
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else if (hasRaw) {
|
|
71
|
+
// Remove .raw: item.raw.x -> item.x
|
|
72
|
+
for (const ref of destructuredItemRefs) {
|
|
73
|
+
count += removeDotMember(ref.reference, 'raw')
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Process variable refs (e.g. v-slot="data")
|
|
79
|
+
for (const ref of variableRefs) {
|
|
80
|
+
// ref.reference.parent is the `data.item` MemberExpression
|
|
81
|
+
const itemMember = ref.reference.parent
|
|
82
|
+
if (
|
|
83
|
+
itemMember?.type === 'MemberExpression'
|
|
84
|
+
&& itemMember.parent?.type === 'MemberExpression'
|
|
85
|
+
&& itemMember.parent.property.type === 'Identifier'
|
|
86
|
+
&& itemMember.parent.property.name === 'raw'
|
|
87
|
+
) {
|
|
88
|
+
// Remove .raw: data.item.raw.x -> data.item.x
|
|
89
|
+
count += removeDotMember(ref.reference, 'raw')
|
|
90
|
+
} else {
|
|
91
|
+
// Rename data.item -> data.internalItem
|
|
92
|
+
ref.reference.name = 'internalItem'
|
|
93
|
+
count++
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return count
|
|
99
|
+
},
|
|
100
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { expect, it } from 'vitest'
|
|
2
|
+
import { transform } from 'vue-metamorph'
|
|
3
|
+
import { v4ElevationPlugin } from './elevation'
|
|
4
|
+
|
|
5
|
+
it('updates elevation values', () => {
|
|
6
|
+
const input = `
|
|
7
|
+
<template>
|
|
8
|
+
<VAlert elevation="5" />
|
|
9
|
+
<div class="elevation-23" />
|
|
10
|
+
<VMenu content-class="bg-surface elevation-16" />
|
|
11
|
+
</template>
|
|
12
|
+
`
|
|
13
|
+
|
|
14
|
+
expect(transform(input, 'file.vue', [v4ElevationPlugin]).code).toMatchInlineSnapshot(`
|
|
15
|
+
"
|
|
16
|
+
<template>
|
|
17
|
+
<VAlert elevation="2" />
|
|
18
|
+
<div class="elevation-5" />
|
|
19
|
+
<VMenu content-class="bg-surface elevation-4" />
|
|
20
|
+
</template>
|
|
21
|
+
"
|
|
22
|
+
`)
|
|
23
|
+
})
|