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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { CodemodPlugin } from 'vue-metamorph'
|
|
2
|
+
import { camelize, classify, findClassNodes } from '../../helpers'
|
|
3
|
+
|
|
4
|
+
const elevationComponents = new Set([
|
|
5
|
+
'VAlert',
|
|
6
|
+
'VAppBar',
|
|
7
|
+
'VAppBarNavIcon',
|
|
8
|
+
'VBanner',
|
|
9
|
+
'VBottomNavigation',
|
|
10
|
+
'VBtn',
|
|
11
|
+
'VBtnGroup',
|
|
12
|
+
'VBtnToggle',
|
|
13
|
+
'VCard',
|
|
14
|
+
'VChip',
|
|
15
|
+
'VColorInput',
|
|
16
|
+
'VColorPicker',
|
|
17
|
+
'VDateInput',
|
|
18
|
+
'VDatePicker',
|
|
19
|
+
'VExpansionPanel',
|
|
20
|
+
'VExpansionPanels',
|
|
21
|
+
'VFab',
|
|
22
|
+
'VFileUpload',
|
|
23
|
+
'VFileUploadItem',
|
|
24
|
+
'VFooter',
|
|
25
|
+
'VHotkey',
|
|
26
|
+
'VIconBtn',
|
|
27
|
+
'VKbd',
|
|
28
|
+
'VList',
|
|
29
|
+
'VListItem',
|
|
30
|
+
'VNavigationDrawer',
|
|
31
|
+
'VPagination',
|
|
32
|
+
'VPicker',
|
|
33
|
+
'VRangeSlider',
|
|
34
|
+
'VSheet',
|
|
35
|
+
'VSkeletonLoader',
|
|
36
|
+
'VSlider',
|
|
37
|
+
'VStepper',
|
|
38
|
+
'VStepperVertical',
|
|
39
|
+
'VStepperVerticalItem',
|
|
40
|
+
'VSystemBar',
|
|
41
|
+
'VTab',
|
|
42
|
+
'VTimePicker',
|
|
43
|
+
'VTimelineItem',
|
|
44
|
+
'VToolbar',
|
|
45
|
+
'VTreeview',
|
|
46
|
+
'VTreeviewItem',
|
|
47
|
+
'VVideo',
|
|
48
|
+
'VVideoControls',
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
const mapping = [0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5]
|
|
52
|
+
|
|
53
|
+
const elevationMatch = String.raw`elevation-(\d{1,2})`
|
|
54
|
+
const elevationRegexp = new RegExp(String.raw`(^|\s)${elevationMatch}(?=$|\s)`, 'g')
|
|
55
|
+
|
|
56
|
+
export const v4ElevationPlugin: CodemodPlugin = {
|
|
57
|
+
type: 'codemod',
|
|
58
|
+
name: 'vuetify-4-elevation',
|
|
59
|
+
transform ({ sfcAST, utils }) {
|
|
60
|
+
if (!sfcAST) return 0
|
|
61
|
+
let count = 0
|
|
62
|
+
|
|
63
|
+
// Match classes
|
|
64
|
+
const found = findClassNodes(sfcAST, utils, [elevationMatch])
|
|
65
|
+
for (const node of found) {
|
|
66
|
+
if (node.type === 'Identifier') {
|
|
67
|
+
node.name = node.name.replaceAll(elevationRegexp, (_, s, n) => `${s}elevation-${mapping[Number(n)]}`)
|
|
68
|
+
count++
|
|
69
|
+
} else if (typeof node.value === 'string') {
|
|
70
|
+
node.value = node.value.replaceAll(elevationRegexp, (_, s, n) => `${s}elevation-${mapping[Number(n)]}`)
|
|
71
|
+
count++
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Match elevation props
|
|
76
|
+
utils.traverseTemplateAST(sfcAST, {
|
|
77
|
+
enterNode (node) {
|
|
78
|
+
if (node.type === 'VElement') {
|
|
79
|
+
const elementName = classify(node.rawName)
|
|
80
|
+
for (const attribute of node.startTag.attributes) {
|
|
81
|
+
if (attribute.key.type !== 'VIdentifier') continue
|
|
82
|
+
const attributeName = camelize(attribute.key.name)
|
|
83
|
+
if (
|
|
84
|
+
elevationComponents.has(elementName)
|
|
85
|
+
&& attributeName === 'elevation'
|
|
86
|
+
&& attribute.value?.type === 'VLiteral'
|
|
87
|
+
) {
|
|
88
|
+
attribute.value.value = String(mapping[Number(attribute.value.value)])
|
|
89
|
+
count++
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
return count
|
|
96
|
+
},
|
|
97
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { expect, it } from 'vitest'
|
|
2
|
+
import { transform } from 'vue-metamorph'
|
|
3
|
+
import { v4FormSlotRefsPlugin } from './form-slot-refs'
|
|
4
|
+
|
|
5
|
+
it('destructured', () => {
|
|
6
|
+
const input = `
|
|
7
|
+
<template>
|
|
8
|
+
<VForm>
|
|
9
|
+
<template #default="{ isValid }">
|
|
10
|
+
{{ isValid.value }}
|
|
11
|
+
</template>
|
|
12
|
+
</VForm>
|
|
13
|
+
</template>
|
|
14
|
+
`
|
|
15
|
+
|
|
16
|
+
expect(transform(input, 'file.vue', [v4FormSlotRefsPlugin]).code).toMatchInlineSnapshot(`
|
|
17
|
+
"
|
|
18
|
+
<template>
|
|
19
|
+
<VForm>
|
|
20
|
+
<template #default="{ isValid }">
|
|
21
|
+
{{ isValid }}
|
|
22
|
+
</template>
|
|
23
|
+
</VForm>
|
|
24
|
+
</template>
|
|
25
|
+
"
|
|
26
|
+
`)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('variable', () => {
|
|
30
|
+
const input = `
|
|
31
|
+
<template>
|
|
32
|
+
<VForm>
|
|
33
|
+
<template #default="data">
|
|
34
|
+
{{ data.isValid.value }}
|
|
35
|
+
</template>
|
|
36
|
+
</VForm>
|
|
37
|
+
</template>
|
|
38
|
+
`
|
|
39
|
+
|
|
40
|
+
expect(transform(input, 'file.vue', [v4FormSlotRefsPlugin]).code).toMatchInlineSnapshot(`
|
|
41
|
+
"
|
|
42
|
+
<template>
|
|
43
|
+
<VForm>
|
|
44
|
+
<template #default="data">
|
|
45
|
+
{{ data.isValid }}
|
|
46
|
+
</template>
|
|
47
|
+
</VForm>
|
|
48
|
+
</template>
|
|
49
|
+
"
|
|
50
|
+
`)
|
|
51
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CodemodPlugin } from 'vue-metamorph'
|
|
2
|
+
import { findSlotNodes, findSlotPropReferences, removeDotMember } from '../../helpers'
|
|
3
|
+
|
|
4
|
+
export const v4FormSlotRefsPlugin: CodemodPlugin = {
|
|
5
|
+
type: 'codemod',
|
|
6
|
+
name: 'vuetify-4-form-slot-refs',
|
|
7
|
+
transform ({ sfcAST }) {
|
|
8
|
+
if (!sfcAST) return 0
|
|
9
|
+
let count = 0
|
|
10
|
+
const slotNodes = findSlotNodes(sfcAST, ['VForm'], ['default'])
|
|
11
|
+
for (const node of slotNodes) {
|
|
12
|
+
const refs = findSlotPropReferences(
|
|
13
|
+
node,
|
|
14
|
+
['errors', 'isDisabled', 'isReadonly', 'isValidating', 'isValid', 'items'],
|
|
15
|
+
)
|
|
16
|
+
for (const ref of refs) {
|
|
17
|
+
count += removeDotMember(ref.reference, 'value')
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return count
|
|
21
|
+
},
|
|
22
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { transform } from 'vue-metamorph'
|
|
3
|
+
import { v4GridPlugin } from './grid'
|
|
4
|
+
|
|
5
|
+
describe('VRow', () => {
|
|
6
|
+
describe('dense', () => {
|
|
7
|
+
it('converts dense to density="compact"', () => {
|
|
8
|
+
const input = `
|
|
9
|
+
<template>
|
|
10
|
+
<v-row dense />
|
|
11
|
+
<v-row dense><v-col /></v-row>
|
|
12
|
+
</template>
|
|
13
|
+
`
|
|
14
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
15
|
+
"
|
|
16
|
+
<template>
|
|
17
|
+
<v-row density="compact" />
|
|
18
|
+
<v-row density="compact"><v-col /></v-row>
|
|
19
|
+
</template>
|
|
20
|
+
"
|
|
21
|
+
`)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('leaves :dense binding untouched', () => {
|
|
25
|
+
const input = `
|
|
26
|
+
<template>
|
|
27
|
+
<v-row :dense="condition" />
|
|
28
|
+
</template>
|
|
29
|
+
`
|
|
30
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
31
|
+
"
|
|
32
|
+
<template>
|
|
33
|
+
<v-row :dense="condition" />
|
|
34
|
+
</template>
|
|
35
|
+
"
|
|
36
|
+
`)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('alignment props', () => {
|
|
41
|
+
it('converts align, justify, align-content to utility classes', () => {
|
|
42
|
+
const input = `
|
|
43
|
+
<template>
|
|
44
|
+
<v-row align="center" justify="space-between" />
|
|
45
|
+
<v-row align-content="start" />
|
|
46
|
+
</template>
|
|
47
|
+
`
|
|
48
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
49
|
+
"
|
|
50
|
+
<template>
|
|
51
|
+
<v-row class="align-center justify-space-between" />
|
|
52
|
+
<v-row class="align-content-start" />
|
|
53
|
+
</template>
|
|
54
|
+
"
|
|
55
|
+
`)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('converts responsive breakpoint variants', () => {
|
|
59
|
+
const input = `
|
|
60
|
+
<template>
|
|
61
|
+
<v-row align-sm="start" align-md="center" justify-lg="end" />
|
|
62
|
+
</template>
|
|
63
|
+
`
|
|
64
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
65
|
+
"
|
|
66
|
+
<template>
|
|
67
|
+
<v-row class="align-sm-start align-md-center justify-lg-end" />
|
|
68
|
+
</template>
|
|
69
|
+
"
|
|
70
|
+
`)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('appends to an existing class attribute', () => {
|
|
74
|
+
const input = `
|
|
75
|
+
<template>
|
|
76
|
+
<v-row class="ma-4" align="center" justify="end" />
|
|
77
|
+
</template>
|
|
78
|
+
`
|
|
79
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
80
|
+
"
|
|
81
|
+
<template>
|
|
82
|
+
<v-row class="ma-4 align-center justify-end" />
|
|
83
|
+
</template>
|
|
84
|
+
"
|
|
85
|
+
`)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('leaves dynamic align/justify bindings untouched', () => {
|
|
89
|
+
const input = `
|
|
90
|
+
<template>
|
|
91
|
+
<v-row :align="direction" :justify="condition ? 'start' : 'end'" />
|
|
92
|
+
</template>
|
|
93
|
+
`
|
|
94
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
95
|
+
"
|
|
96
|
+
<template>
|
|
97
|
+
<v-row :align="direction" :justify="condition ? 'start' : 'end'" />
|
|
98
|
+
</template>
|
|
99
|
+
"
|
|
100
|
+
`)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('does not transform align/justify on non-VRow elements', () => {
|
|
104
|
+
const input = `
|
|
105
|
+
<template>
|
|
106
|
+
<v-container align="center" />
|
|
107
|
+
<div justify="end" />
|
|
108
|
+
</template>
|
|
109
|
+
`
|
|
110
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
111
|
+
"
|
|
112
|
+
<template>
|
|
113
|
+
<v-container align="center" />
|
|
114
|
+
<div justify="end" />
|
|
115
|
+
</template>
|
|
116
|
+
"
|
|
117
|
+
`)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('combined', () => {
|
|
122
|
+
it('handles dense + alignment props together', () => {
|
|
123
|
+
const input = `
|
|
124
|
+
<template>
|
|
125
|
+
<v-row dense align="center" justify-md="end" />
|
|
126
|
+
</template>
|
|
127
|
+
`
|
|
128
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
129
|
+
"
|
|
130
|
+
<template>
|
|
131
|
+
<v-row class="align-center justify-md-end" density="compact" />
|
|
132
|
+
</template>
|
|
133
|
+
"
|
|
134
|
+
`)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('leaves unrelated props untouched', () => {
|
|
138
|
+
const input = `
|
|
139
|
+
<template>
|
|
140
|
+
<v-row no-gutters tag="section" />
|
|
141
|
+
</template>
|
|
142
|
+
`
|
|
143
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
144
|
+
"
|
|
145
|
+
<template>
|
|
146
|
+
<v-row no-gutters tag="section" />
|
|
147
|
+
</template>
|
|
148
|
+
"
|
|
149
|
+
`)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('VCol', () => {
|
|
155
|
+
describe('order', () => {
|
|
156
|
+
it('converts order prop to utility class', () => {
|
|
157
|
+
const input = `
|
|
158
|
+
<template>
|
|
159
|
+
<v-col order="2" />
|
|
160
|
+
</template>
|
|
161
|
+
`
|
|
162
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
163
|
+
"
|
|
164
|
+
<template>
|
|
165
|
+
<v-col class="order-2" />
|
|
166
|
+
</template>
|
|
167
|
+
"
|
|
168
|
+
`)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('converts responsive order props', () => {
|
|
172
|
+
const input = `
|
|
173
|
+
<template>
|
|
174
|
+
<v-col order="2" order-md="1" order-lg="3" />
|
|
175
|
+
</template>
|
|
176
|
+
`
|
|
177
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
178
|
+
"
|
|
179
|
+
<template>
|
|
180
|
+
<v-col class="order-2 order-md-1 order-lg-3" />
|
|
181
|
+
</template>
|
|
182
|
+
"
|
|
183
|
+
`)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('leaves dynamic order binding untouched', () => {
|
|
187
|
+
const input = `
|
|
188
|
+
<template>
|
|
189
|
+
<v-col :order="condition ? 1 : 2" />
|
|
190
|
+
</template>
|
|
191
|
+
`
|
|
192
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
193
|
+
"
|
|
194
|
+
<template>
|
|
195
|
+
<v-col :order="condition ? 1 : 2" />
|
|
196
|
+
</template>
|
|
197
|
+
"
|
|
198
|
+
`)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('align-self', () => {
|
|
203
|
+
it('converts align-self prop to utility class', () => {
|
|
204
|
+
const input = `
|
|
205
|
+
<template>
|
|
206
|
+
<v-col align-self="center" />
|
|
207
|
+
<v-col align-self="start" />
|
|
208
|
+
</template>
|
|
209
|
+
`
|
|
210
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
211
|
+
"
|
|
212
|
+
<template>
|
|
213
|
+
<v-col class="align-self-center" />
|
|
214
|
+
<v-col class="align-self-start" />
|
|
215
|
+
</template>
|
|
216
|
+
"
|
|
217
|
+
`)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('leaves dynamic align-self binding untouched', () => {
|
|
221
|
+
const input = `
|
|
222
|
+
<template>
|
|
223
|
+
<v-col :align-self="alignment" />
|
|
224
|
+
</template>
|
|
225
|
+
`
|
|
226
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
227
|
+
"
|
|
228
|
+
<template>
|
|
229
|
+
<v-col :align-self="alignment" />
|
|
230
|
+
</template>
|
|
231
|
+
"
|
|
232
|
+
`)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('combined', () => {
|
|
237
|
+
it('combines order and align-self into a single class attribute', () => {
|
|
238
|
+
const input = `
|
|
239
|
+
<template>
|
|
240
|
+
<v-col order="3" align-self="end" />
|
|
241
|
+
</template>
|
|
242
|
+
`
|
|
243
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
244
|
+
"
|
|
245
|
+
<template>
|
|
246
|
+
<v-col class="order-3 align-self-end" />
|
|
247
|
+
</template>
|
|
248
|
+
"
|
|
249
|
+
`)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('appends to an existing class attribute', () => {
|
|
253
|
+
const input = `
|
|
254
|
+
<template>
|
|
255
|
+
<v-col class="pa-2" order="1" align-self="center" />
|
|
256
|
+
</template>
|
|
257
|
+
`
|
|
258
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
259
|
+
"
|
|
260
|
+
<template>
|
|
261
|
+
<v-col class="pa-2 order-1 align-self-center" />
|
|
262
|
+
</template>
|
|
263
|
+
"
|
|
264
|
+
`)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('leaves span/offset props untouched', () => {
|
|
268
|
+
const input = `
|
|
269
|
+
<template>
|
|
270
|
+
<v-col cols="6" sm="4" offset="2" offset-md="1" />
|
|
271
|
+
</template>
|
|
272
|
+
`
|
|
273
|
+
expect(transform(input, 'file.vue', [v4GridPlugin]).code).toMatchInlineSnapshot(`
|
|
274
|
+
"
|
|
275
|
+
<template>
|
|
276
|
+
<v-col cols="6" sm="4" offset="2" offset-md="1" />
|
|
277
|
+
</template>
|
|
278
|
+
"
|
|
279
|
+
`)
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { AST, CodemodPlugin, CodemodPluginContext } from 'vue-metamorph'
|
|
2
|
+
import { astHelpers } from 'vue-metamorph'
|
|
3
|
+
import { classify } from '../../helpers'
|
|
4
|
+
|
|
5
|
+
type Builders = CodemodPluginContext['utils']['builders']
|
|
6
|
+
|
|
7
|
+
const breakpoints = ['sm', 'md', 'lg', 'xl', 'xxl'] as const
|
|
8
|
+
|
|
9
|
+
export const v4GridPlugin: CodemodPlugin = {
|
|
10
|
+
type: 'codemod',
|
|
11
|
+
name: 'vuetify-4-grid',
|
|
12
|
+
transform ({ sfcAST, utils }) {
|
|
13
|
+
if (!sfcAST) return 0
|
|
14
|
+
let count = 0
|
|
15
|
+
const { builders } = utils
|
|
16
|
+
|
|
17
|
+
const elements = astHelpers.findAll(sfcAST, { type: 'VElement' })
|
|
18
|
+
|
|
19
|
+
for (const el of elements) {
|
|
20
|
+
const tag = el.rawName
|
|
21
|
+
|
|
22
|
+
// VRow
|
|
23
|
+
if (classify(tag) === 'VRow') {
|
|
24
|
+
// dense -> density="compact"
|
|
25
|
+
const denseAttr = getStaticAttr(el, 'dense')
|
|
26
|
+
if (denseAttr) {
|
|
27
|
+
const idx = el.startTag.attributes.indexOf(denseAttr)
|
|
28
|
+
el.startTag.attributes.splice(idx, 1,
|
|
29
|
+
builders.vAttribute(
|
|
30
|
+
builders.vIdentifier('density'),
|
|
31
|
+
builders.vLiteral('compact'),
|
|
32
|
+
))
|
|
33
|
+
count++
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// align, justify, align-content (+ breakpoint variants) -> utility classes
|
|
37
|
+
for (const prop of ['align', 'justify', 'align-content'] as const) {
|
|
38
|
+
if (propToClass(el, prop, prop, builders)) count++
|
|
39
|
+
for (const bp of breakpoints) {
|
|
40
|
+
if (propToClass(el, `${prop}-${bp}`, `${prop}-${bp}`, builders)) count++
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// VCol
|
|
46
|
+
if (classify(tag) === 'VCol') {
|
|
47
|
+
// order (+ breakpoint variants) -> utility classes
|
|
48
|
+
if (propToClass(el, 'order', 'order', builders)) count++
|
|
49
|
+
for (const bp of breakpoints) {
|
|
50
|
+
if (propToClass(el, `order-${bp}`, `order-${bp}`, builders)) count++
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// align-self -> utility class
|
|
54
|
+
if (propToClass(el, 'align-self', 'align-self', builders)) count++
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return count
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getStaticAttr (el: AST.VElement, name: string) {
|
|
63
|
+
return el.startTag.attributes.find(
|
|
64
|
+
a => !a.directive && a.key.name === name,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function removeAttr (el: AST.VElement, name: string) {
|
|
69
|
+
const idx = el.startTag.attributes.findIndex(
|
|
70
|
+
a => !a.directive && a.key.name === name,
|
|
71
|
+
)
|
|
72
|
+
if (~idx) el.startTag.attributes.splice(idx, 1)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function appendStaticClass (el: AST.VElement, cls: string, builders: Builders) {
|
|
76
|
+
const attr = getStaticAttr(el, 'class')
|
|
77
|
+
if (attr?.value) {
|
|
78
|
+
if (attr.value.type === 'VLiteral') {
|
|
79
|
+
attr.value.value = attr.value.value ? `${attr.value.value} ${cls}` : cls
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
el.startTag.attributes.unshift(
|
|
83
|
+
builders.vAttribute(
|
|
84
|
+
builders.vIdentifier('class'),
|
|
85
|
+
builders.vLiteral(cls),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function propToClass (
|
|
92
|
+
el: AST.VElement,
|
|
93
|
+
propName: string,
|
|
94
|
+
classPrefix: string,
|
|
95
|
+
builders: Builders,
|
|
96
|
+
): boolean {
|
|
97
|
+
const attr = getStaticAttr(el, propName)
|
|
98
|
+
if (attr?.value?.type !== 'VLiteral' || !attr.value.value) return false
|
|
99
|
+
|
|
100
|
+
appendStaticClass(el, `${classPrefix}-${attr.value.value}`, builders)
|
|
101
|
+
removeAttr(el, propName)
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Plugin } from 'vue-metamorph'
|
|
2
|
+
import { v4ComboboxItemSlotPlugin } from './combobox-item-slot'
|
|
3
|
+
import { v4ElevationPlugin } from './elevation'
|
|
4
|
+
import { v4SnackbarMultilinePlugin } from './snackbar-multiline'
|
|
5
|
+
import { v4SnackbarQueueSlotPlugin } from './snackbar-queue-slot'
|
|
6
|
+
import { v4TypographyPlugin } from './typography'
|
|
7
|
+
|
|
8
|
+
export function vuetify4 (): Plugin[] {
|
|
9
|
+
return [
|
|
10
|
+
v4ComboboxItemSlotPlugin,
|
|
11
|
+
v4ElevationPlugin,
|
|
12
|
+
v4SnackbarMultilinePlugin,
|
|
13
|
+
v4SnackbarQueueSlotPlugin,
|
|
14
|
+
v4TypographyPlugin,
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { expect, it } from 'vitest'
|
|
2
|
+
import { transform } from 'vue-metamorph'
|
|
3
|
+
import { v4SnackbarMultilinePlugin } from './snackbar-multiline'
|
|
4
|
+
|
|
5
|
+
it('replaces multi-line with min-height', () => {
|
|
6
|
+
const input = `
|
|
7
|
+
<template>
|
|
8
|
+
<VSnackbar multi-line />
|
|
9
|
+
</template>
|
|
10
|
+
`
|
|
11
|
+
|
|
12
|
+
expect(transform(input, 'file.vue', [v4SnackbarMultilinePlugin]).code).toMatchInlineSnapshot(`
|
|
13
|
+
"
|
|
14
|
+
<template>
|
|
15
|
+
<VSnackbar min-height="68" />
|
|
16
|
+
</template>
|
|
17
|
+
"
|
|
18
|
+
`)
|
|
19
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CodemodPlugin } from 'vue-metamorph'
|
|
2
|
+
import { camelize, classify } from '../../helpers'
|
|
3
|
+
|
|
4
|
+
export const v4SnackbarMultilinePlugin: CodemodPlugin = {
|
|
5
|
+
type: 'codemod',
|
|
6
|
+
name: 'vuetify-4-snackbar-multiline',
|
|
7
|
+
transform ({ sfcAST, utils: { builders, traverseTemplateAST } }) {
|
|
8
|
+
if (!sfcAST) return 0
|
|
9
|
+
let count = 0
|
|
10
|
+
traverseTemplateAST(sfcAST, {
|
|
11
|
+
enterNode (node) {
|
|
12
|
+
if (
|
|
13
|
+
node.type !== 'VElement'
|
|
14
|
+
|| classify(node.rawName) !== 'VSnackbar'
|
|
15
|
+
) return
|
|
16
|
+
for (const attribute of node.startTag.attributes) {
|
|
17
|
+
if (attribute.key.type !== 'VIdentifier') continue
|
|
18
|
+
if (camelize(attribute.key.name) === 'multiLine') {
|
|
19
|
+
node.startTag.attributes = node.startTag.attributes.filter(attr => attr !== attribute)
|
|
20
|
+
|
|
21
|
+
const hasMinHeight = node.startTag.attributes.some(attr =>
|
|
22
|
+
attr.key.type === 'VIdentifier'
|
|
23
|
+
&& camelize(attr.key.name) === 'minHeight',
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if (!hasMinHeight) {
|
|
27
|
+
node.startTag.attributes.push(
|
|
28
|
+
builders.vAttribute(builders.vIdentifier('min-height'), builders.vLiteral('68')),
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
count++
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
return count
|
|
39
|
+
},
|
|
40
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { expect, it } from 'vitest'
|
|
2
|
+
import { transform } from 'vue-metamorph'
|
|
3
|
+
import { v4SnackbarQueueSlotPlugin } from './snackbar-queue-slot'
|
|
4
|
+
|
|
5
|
+
it('renames default slot to item', () => {
|
|
6
|
+
const input = `
|
|
7
|
+
<template>
|
|
8
|
+
<VSnackbarQueue>
|
|
9
|
+
<template #default="{ item }">
|
|
10
|
+
<v-snackbar v-bind="item" />
|
|
11
|
+
</template>
|
|
12
|
+
</VSnackbarQueue>
|
|
13
|
+
</template>
|
|
14
|
+
`
|
|
15
|
+
|
|
16
|
+
expect(transform(input, 'file.vue', [v4SnackbarQueueSlotPlugin]).code).toMatchInlineSnapshot(`
|
|
17
|
+
"
|
|
18
|
+
<template>
|
|
19
|
+
<VSnackbarQueue>
|
|
20
|
+
<template #item="{ item }">
|
|
21
|
+
<v-snackbar v-bind="item" />
|
|
22
|
+
</template>
|
|
23
|
+
</VSnackbarQueue>
|
|
24
|
+
</template>
|
|
25
|
+
"
|
|
26
|
+
`)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('works without template', () => {
|
|
30
|
+
const input = `
|
|
31
|
+
<template>
|
|
32
|
+
<VSnackbarQueue v-slot="{ item }">
|
|
33
|
+
<v-snackbar v-bind="item" />
|
|
34
|
+
</VSnackbarQueue>
|
|
35
|
+
<VSnackbarQueue #default="{ item }">
|
|
36
|
+
<v-snackbar v-bind="item" />
|
|
37
|
+
</VSnackbarQueue>
|
|
38
|
+
</template>
|
|
39
|
+
`
|
|
40
|
+
|
|
41
|
+
expect(transform(input, 'file.vue', [v4SnackbarQueueSlotPlugin]).code).toMatchInlineSnapshot(`
|
|
42
|
+
"
|
|
43
|
+
<template>
|
|
44
|
+
<VSnackbarQueue #item="{ item }">
|
|
45
|
+
<v-snackbar v-bind="item" />
|
|
46
|
+
</VSnackbarQueue>
|
|
47
|
+
<VSnackbarQueue #item="{ item }">
|
|
48
|
+
<v-snackbar v-bind="item" />
|
|
49
|
+
</VSnackbarQueue>
|
|
50
|
+
</template>
|
|
51
|
+
"
|
|
52
|
+
`)
|
|
53
|
+
})
|