UnrefinedBrain / vue-metamorph

Codemod Framework for Vue projects
https://vue-metamorph.dev/
MIT License
72 stars 1 forks source link

the range of VForExpression is different from the AST explorer #102

Closed haoliangwu closed 2 weeks ago

haoliangwu commented 1 month ago

see following screenshots:

image image

as you can see, the range of VForExpression in AST explorer is [57, 77], but the range of vue-metamorph(maybe resolved by eslint-vue-parser) is [57, 78].

the inconsistent here cause a bug in my plugin which I plan to apply :key directive to host element when there is only v-for on it:

image

the end double quota of v-for is missing.

I tried to read source code to find out the root cause here, but I haven't yet, anyway, I found the sfcAST is provided by vue-eslint-parser, https://github.com/UnrefinedBrain/vue-metamorph/blob/e388938d64f2d0df9eed97672e630bae37c5831d/src/parse/vue.ts#L22.

so maybe this issue is related to vue-eslint-parser itself rather than vue-metamorph?

FYI, the source code of plugin and test case for debug purpose:

// plugin.ts
import { namedTypes as n } from 'ast-types'
import { CodemodPlugin, AST } from 'vue-metamorph'

const cwd = process.cwd()

const checkKeyProp = (node: AST.VAttribute | AST.VDirective) => {
  return (
    node.key.type === 'VDirectiveKey' && node.key.argument?.type === 'VIdentifier' && node.key.argument?.name === 'key'
  )
}

export const vueRequireVForKeyCodemod: CodemodPlugin = {
  type: 'codemod',
  name: 'vue-require-v-for-key',
  transform({ scriptASTs, sfcAST, utils: { traverseScriptAST, traverseTemplateAST, astHelpers, builders }, opts }) {
    let transformCount = 0

    function fix(hostElement: AST.VElement, directive: AST.VDirective, keyPrefix?: string) {
      const hasKeyProp = hostElement.startTag.attributes.some(node => checkKeyProp(node))

      if (hasKeyProp) {
        return
      } else {
        let indexIdentifier

        if (directive.value?.expression?.type === 'VForExpression') {
          const vForExpression = directive.value?.expression

          if (vForExpression.left.length > 1) {
            indexIdentifier = vForExpression.left[1] as n.Identifier
          } else {
            indexIdentifier = builders.identifier('index')
            vForExpression.left.push(indexIdentifier)

            transformCount++
          }
        }

        hostElement.startTag.attributes.push(
          builders.vDirective(
            builders.vDirectiveKey(builders.vIdentifier('bind', ':'), builders.vIdentifier('key', 'key')),
            builders.vExpressionContainer(
              keyPrefix
                ? builders.binaryExpression(
                    '+',
                    builders.literal(keyPrefix),
                    builders.identifier(indexIdentifier!.name),
                  )
                : builders.identifier(indexIdentifier!.name),
            ),
          ),
        )

        transformCount++
      }
    }

    if (sfcAST) {
      traverseTemplateAST(sfcAST, {
        enterNode(node) {
          if (node.type === 'VIdentifier' && node.name === 'for') {
            const directive = (node.parent as AST.VDirectiveKey).parent
            const hostElement = node.parent.parent.parent.parent as AST.VElement

            const isReservedTag = hostElement.rawName === 'template' || hostElement.rawName === 'slot'

            if (isReservedTag) {
              hostElement.children
                .filter(e => e.type === 'VElement')
                .forEach((childHostElement, index) => {
                  fix(childHostElement as AST.VElement, directive, String(index))
                })
            } else {
              fix(hostElement, directive)
            }
          }
        },
      })
    }

    return transformCount
  },
}
test('v-for on template', () => {
    const source = `<template>
  <div class="TeamInfo">
    <template v-for="item in teamInfoList">
      <bTypography variant="caption" component="dt">{{ item.dt }}</bTypography>
    </template>
  </div>
</template>
  `

    const expected = `<template>
  <div class="TeamInfo">
    <template v-for="(item, index) in teamInfoList">
      <bTypography variant="caption" component="dt" :key="'0' + index">{{ item.dt }}</bTypography>
    </template>
  </div>
</template>
    `

    expect(transform(source, 'file.vue', [vueRequireVForKeyCodemod], baseOptions).code).toBe(expected)
  })
UnrefinedBrain commented 1 month ago

First thing that jumped out at me was that AST Explorer uses vue-eslint-parser 7.6.0, and vue-metamorph has 9.4.2. I downgraded to 7.6.0 in vue-metamorph and your test case started passing.

Through a bisect of vue-eslint-parser versions, I found that the version where your test case starts failing is vue-eslint-parser 9.1.0. 9.0.3 is the last version that your test case passes on

I don't see anything immediately obvious in the diff, but I will try to dig deeper: https://github.com/vuejs/vue-eslint-parser/compare/v9.0.3..v9.1.0

at the least, if you can force a resolution to vue-eslint-parser 9.0.3, it might solve your problem for now

haoliangwu commented 1 month ago

okay, I also noticed that after downgrading the vue-eslint-parser this morning and the all failed testcase passed, so I will take it as workaround currently.

anyway, thanks for your patient answer and investigate. as now there are something wrong with vue-eslint-parser itself, I will also try to dig it deeper later and share with you once I find something useful.

UnrefinedBrain commented 2 weeks ago

I found a workaround. should be fixed in v3.1.10

Can you try it and let me know if it works for you?

haoliangwu commented 21 hours ago

@UnrefinedBrain okay, I also have a workaround with local patch on my device, I will remove it and test it again.

haoliangwu commented 21 hours ago

works as expected of vue-metamorph@3.1.15, see: image

thanks for you awesome works.

do you have donation link like Buy Me A Coffee or Patreon, I'd like to buy you a coffee, because this project saves my lots of time for migration nuxt 2 project.