vuetify-codemods 1.0.0 → 1.0.1

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 CHANGED
@@ -15,3 +15,13 @@ npx vuetify-codemods@latest
15
15
  #### `--files <glob>`
16
16
 
17
17
  Default: `**/src/**/*`
18
+
19
+ ## Included rules
20
+
21
+ - [Combobox item slot](https://vuetifyjs.com/en/getting-started/upgrade-guide/#vselect-vcombobox-vautocomplete)
22
+ - [Elevation](https://vuetifyjs.com/en/getting-started/upgrade-guide/#elevation)
23
+ - [Form slot refs](https://vuetifyjs.com/en/getting-started/upgrade-guide/#vform)
24
+ - [Grid](https://vuetifyjs.com/en/getting-started/upgrade-guide/#grid-system-vrow-and-vcol)
25
+ - [Snackbar multi-line](https://vuetifyjs.com/en/getting-started/upgrade-guide/#vsnackbar)
26
+ - [SnackbarQueue default slot](https://vuetifyjs.com/en/getting-started/upgrade-guide/#vsnackbarqueue)
27
+ - [Typography](https://vuetifyjs.com/en/getting-started/upgrade-guide/#typography)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vuetify-codemods",
3
3
  "type": "module",
4
- "version": "1.0.0",
4
+ "version": "1.0.1",
5
5
  "bin": {
6
6
  "vuetify-codemods": "dist/main.js"
7
7
  },
@@ -16,6 +16,9 @@
16
16
  "type": "git",
17
17
  "url": "git+https://github.com/vuetifyjs/vuetify-codemods.git"
18
18
  },
19
+ "files": [
20
+ "dist"
21
+ ],
19
22
  "dependencies": {
20
23
  "@inquirer/prompts": "^8.3.0",
21
24
  "esprima": "^4.0.1",
@@ -25,6 +28,8 @@
25
28
  "devDependencies": {
26
29
  "@types/node": "^24.10.13",
27
30
  "ast-types-x": "^1.18.0",
31
+ "conventional-changelog-cli": "^5.0.0",
32
+ "conventional-changelog-vuetify": "^2.0.2",
28
33
  "eslint": "^9.39.2",
29
34
  "eslint-config-vuetify": "4.3.5-beta.1",
30
35
  "typescript": "^5.9.3",
package/.nvmrc DELETED
@@ -1 +0,0 @@
1
- 24.12.0
package/eslint.config.js DELETED
@@ -1,9 +0,0 @@
1
- import vuetify from 'eslint-config-vuetify'
2
-
3
- export default vuetify({
4
- rules: {
5
- 'curly': ['error', 'multi-line', 'consistent'],
6
- 'complexity': 'off',
7
- 'unicorn/no-process-exit': 'off',
8
- },
9
- })
@@ -1,38 +0,0 @@
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
- }
@@ -1,5 +0,0 @@
1
- ignoredBuiltDependencies:
2
- - esbuild
3
-
4
- patchedDependencies:
5
- vue-metamorph: patches/vue-metamorph.patch
package/src/helpers.ts DELETED
@@ -1,259 +0,0 @@
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 DELETED
@@ -1,83 +0,0 @@
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
- }
@@ -1,103 +0,0 @@
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
- })
@@ -1,100 +0,0 @@
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
- }
@@ -1,23 +0,0 @@
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
- })