]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: parser v2 compat
authorEvan You <yyx990803@gmail.com>
Wed, 22 Nov 2023 07:19:06 +0000 (15:19 +0800)
committerEvan You <yyx990803@gmail.com>
Sat, 25 Nov 2023 08:18:29 +0000 (16:18 +0800)
packages/compiler-core/src/compat/compatConfig.ts
packages/compiler-core/src/parser/index.ts
packages/vue-compat/__tests__/compiler.spec.ts

index b643e801117b6ca06b6a0b4dd6b8beb77f51a509..1ca59b5b31df8503c2aa4563e5f43387dd51c984 100644 (file)
@@ -1,7 +1,6 @@
 import { SourceLocation } from '../ast'
 import { CompilerError } from '../errors'
-// @ts-expect-error TODO
-import { ParserContext } from '../parse'
+import { MergedParserOptions } from '../parser'
 import { TransformContext } from '../transform'
 
 export type CompilerCompatConfig = Partial<
@@ -17,7 +16,6 @@ export interface CompilerCompatOptions {
 export const enum CompilerDeprecationTypes {
   COMPILER_IS_ON_ELEMENT = 'COMPILER_IS_ON_ELEMENT',
   COMPILER_V_BIND_SYNC = 'COMPILER_V_BIND_SYNC',
-  COMPILER_V_BIND_PROP = 'COMPILER_V_BIND_PROP',
   COMPILER_V_BIND_OBJECT_ORDER = 'COMPILER_V_BIND_OBJECT_ORDER',
   COMPILER_V_ON_NATIVE = 'COMPILER_V_ON_NATIVE',
   COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE',
@@ -48,12 +46,6 @@ const deprecationData: Record<CompilerDeprecationTypes, DeprecationData> = {
     link: `https://v3-migration.vuejs.org/breaking-changes/v-model.html`
   },
 
-  [CompilerDeprecationTypes.COMPILER_V_BIND_PROP]: {
-    message:
-      `.prop modifier for v-bind has been removed and no longer necessary. ` +
-      `Vue 3 will automatically set a binding as DOM property when appropriate.`
-  },
-
   [CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER]: {
     message:
       `v-bind="obj" usage is now order sensitive and behaves like JavaScript ` +
@@ -101,12 +93,9 @@ const deprecationData: Record<CompilerDeprecationTypes, DeprecationData> = {
 
 function getCompatValue(
   key: CompilerDeprecationTypes | 'MODE',
-  context: ParserContext | TransformContext
+  { compatConfig }: MergedParserOptions | TransformContext
 ) {
-  const config = (context as ParserContext).options
-    ? (context as ParserContext).options.compatConfig
-    : (context as TransformContext).compatConfig
-  const value = config && config[key]
+  const value = compatConfig && compatConfig[key]
   if (key === 'MODE') {
     return value || 3 // compiler defaults to v3 behavior
   } else {
@@ -116,7 +105,7 @@ function getCompatValue(
 
 export function isCompatEnabled(
   key: CompilerDeprecationTypes,
-  context: ParserContext | TransformContext
+  context: MergedParserOptions | TransformContext
 ) {
   const mode = getCompatValue('MODE', context)
   const value = getCompatValue(key, context)
@@ -127,7 +116,7 @@ export function isCompatEnabled(
 
 export function checkCompatEnabled(
   key: CompilerDeprecationTypes,
-  context: ParserContext | TransformContext,
+  context: MergedParserOptions | TransformContext,
   loc: SourceLocation | null,
   ...args: any[]
 ): boolean {
@@ -140,7 +129,7 @@ export function checkCompatEnabled(
 
 export function warnDeprecation(
   key: CompilerDeprecationTypes,
-  context: ParserContext | TransformContext,
+  context: MergedParserOptions | TransformContext,
   loc: SourceLocation | null,
   ...args: any[]
 ) {
index 1aec4c04bdcc6a9e5a647223d765e6ea776fa5ff..72886fbbbdc89abd2ebdadd28557f7aa17b11f0f 100644 (file)
@@ -24,7 +24,13 @@ import Tokenizer, {
   isWhitespace,
   toCharCodes
 } from './Tokenizer'
-import { CompilerCompatOptions } from '../compat/compatConfig'
+import {
+  CompilerCompatOptions,
+  CompilerDeprecationTypes,
+  checkCompatEnabled,
+  isCompatEnabled,
+  warnDeprecation
+} from '../compat/compatConfig'
 import { NO, extend } from '@vue/shared'
 import {
   ErrorCodes,
@@ -32,7 +38,7 @@ import {
   defaultOnError,
   defaultOnWarn
 } from '../errors'
-import { forAliasRE, isCoreComponent } from '../utils'
+import { forAliasRE, isCoreComponent, isStaticArgOf } from '../utils'
 import { decodeHTML } from 'entities/lib/decode.js'
 
 type OptionalOptions =
@@ -42,7 +48,10 @@ type OptionalOptions =
   | 'isBuiltInComponent'
   | keyof CompilerCompatOptions
 
-type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
+export type MergedParserOptions = Omit<
+  Required<ParserOptions>,
+  OptionalOptions
+> &
   Pick<ParserOptions, OptionalOptions>
 
 export const defaultParserOptions: MergedParserOptions = {
@@ -63,7 +72,7 @@ let currentRoot: RootNode | null = null
 
 // parser state
 let currentInput = ''
-let currentElement: ElementNode | null = null
+let currentOpenTag: ElementNode | null = null
 let currentProp: AttributeNode | DirectiveNode | null = null
 let currentAttrValue = ''
 let currentAttrStartIndex = -1
@@ -118,7 +127,7 @@ const tokenizer = new Tokenizer(stack, {
     const startIndex = tokenizer.inSFCRoot
       ? end + fastForward(end, CharCodes.Gt) + 1
       : start - 1
-    currentElement = {
+    currentOpenTag = {
       type: NodeTypes.ELEMENT,
       tag: name,
       ns: currentOptions.getNamespace(name, stack[0], currentOptions.ns),
@@ -159,7 +168,7 @@ const tokenizer = new Tokenizer(stack, {
   },
 
   onselfclosingtag(end) {
-    const name = currentElement!.tag
+    const name = currentOpenTag!.tag
     endOpenTag(end)
     if (stack[0]?.tag === name) {
       onCloseTag(stack.shift()!, end)
@@ -213,9 +222,9 @@ const tokenizer = new Tokenizer(stack, {
       }
       if (name === 'pre') {
         inVPre = true
-        currentVPreBoundary = currentElement
+        currentVPreBoundary = currentOpenTag
         // convert dirs before this one to attributes
-        const props = currentElement!.props
+        const props = currentOpenTag!.props
         for (let i = 0; i < props.length; i++) {
           if (props[i].type === NodeTypes.DIRECTIVE) {
             props[i] = dirToAttr(props[i] as DirectiveNode)
@@ -279,7 +288,7 @@ const tokenizer = new Tokenizer(stack, {
     }
     // check duplicate attrs
     if (
-      currentElement!.props.some(
+      currentOpenTag!.props.some(
         p => (p.type === NodeTypes.DIRECTIVE ? p.rawName : p.name) === name
       )
     ) {
@@ -288,7 +297,10 @@ const tokenizer = new Tokenizer(stack, {
   },
 
   onattribend(quote, end) {
-    if (currentElement && currentProp) {
+    if (currentOpenTag && currentProp) {
+      // finalize end pos
+      currentProp.loc.end = tokenizer.getPos(end)
+
       if (quote !== QuoteType.NoValue) {
         if (__BROWSER__ && currentAttrValue.includes('&')) {
           currentAttrValue = currentOptions.decodeEntities!(
@@ -296,6 +308,7 @@ const tokenizer = new Tokenizer(stack, {
             true
           )
         }
+
         if (currentProp.type === NodeTypes.ATTRIBUTE) {
           // assign value
 
@@ -318,7 +331,7 @@ const tokenizer = new Tokenizer(stack, {
           }
           if (
             tokenizer.inSFCRoot &&
-            currentElement.tag === 'template' &&
+            currentOpenTag.tag === 'template' &&
             currentProp.name === 'lang' &&
             currentAttrValue &&
             currentAttrValue !== 'html'
@@ -338,14 +351,29 @@ const tokenizer = new Tokenizer(stack, {
           if (currentProp.name === 'for') {
             currentProp.forParseResult = parseForExpression(currentProp.exp)
           }
+          // 2.x compat v-bind:foo.sync -> v-model:foo
+          let syncIndex = -1
+          if (
+            __COMPAT__ &&
+            currentProp.name === 'bind' &&
+            (syncIndex = currentProp.modifiers.indexOf('sync')) > -1 &&
+            checkCompatEnabled(
+              CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
+              currentOptions,
+              currentProp.loc,
+              currentProp.rawName
+            )
+          ) {
+            currentProp.name = 'model'
+            currentProp.modifiers.splice(syncIndex, 1)
+          }
         }
       }
-      currentProp.loc.end = tokenizer.getPos(end)
       if (
         currentProp.type !== NodeTypes.DIRECTIVE ||
         currentProp.name !== 'pre'
       ) {
-        currentElement.props.push(currentProp)
+        currentOpenTag.props.push(currentProp)
       }
     }
     currentAttrValue = ''
@@ -503,20 +531,20 @@ function getSlice(start: number, end: number) {
 }
 
 function endOpenTag(end: number) {
-  addNode(currentElement!)
-  const { tag, ns } = currentElement!
+  addNode(currentOpenTag!)
+  const { tag, ns } = currentOpenTag!
   if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
     inPre++
   }
   if (currentOptions.isVoidTag(tag)) {
-    onCloseTag(currentElement!, end)
+    onCloseTag(currentOpenTag!, end)
   } else {
-    stack.unshift(currentElement!)
+    stack.unshift(currentOpenTag!)
     if (ns === Namespaces.SVG || ns === Namespaces.MATH_ML) {
       tokenizer.inXML = true
     }
   }
-  currentElement = null
+  currentOpenTag = null
 }
 
 function onText(content: string, start: number, end: number) {
@@ -586,6 +614,81 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) {
   ) {
     tokenizer.inXML = false
   }
+
+  // 2.x compat / deprecation checks
+  if (__COMPAT__) {
+    const props = el.props
+    if (
+      __DEV__ &&
+      isCompatEnabled(
+        CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
+        currentOptions
+      )
+    ) {
+      let hasIf = false
+      let hasFor = false
+      for (let i = 0; i < props.length; i++) {
+        const p = props[i]
+        if (p.type === NodeTypes.DIRECTIVE) {
+          if (p.name === 'if') {
+            hasIf = true
+          } else if (p.name === 'for') {
+            hasFor = true
+          }
+        }
+        if (hasIf && hasFor) {
+          warnDeprecation(
+            CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
+            currentOptions,
+            el.loc
+          )
+          break
+        }
+      }
+    }
+
+    if (
+      isCompatEnabled(
+        CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
+        currentOptions
+      ) &&
+      el.tag === 'template' &&
+      !isFragmentTemplate(el)
+    ) {
+      __DEV__ &&
+        warnDeprecation(
+          CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
+          currentOptions,
+          el.loc
+        )
+      // unwrap
+      const parent = stack[0] || currentRoot
+      const index = parent.children.indexOf(el)
+      parent.children.splice(index, 1, ...el.children)
+    }
+
+    const inlineTemplateProp = props.find(
+      p => p.type === NodeTypes.ATTRIBUTE && p.name === 'inline-template'
+    ) as AttributeNode
+    if (
+      inlineTemplateProp &&
+      checkCompatEnabled(
+        CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE,
+        currentOptions,
+        inlineTemplateProp.loc
+      ) &&
+      el.children.length
+    ) {
+      inlineTemplateProp.value = {
+        type: NodeTypes.TEXT,
+        content: getSlice(
+          el.children[0].loc.start.offset,
+          el.children[el.children.length - 1].loc.end.offset
+        ),
+        loc: inlineTemplateProp.loc
+      }
+    }
+  }
 }
 
 function fastForward(start: number, c: number) {
@@ -641,32 +744,30 @@ function isComponent({ tag, props }: ElementNode): boolean {
       if (p.name === 'is' && p.value) {
         if (p.value.content.startsWith('vue:')) {
           return true
+        } else if (
+          __COMPAT__ &&
+          checkCompatEnabled(
+            CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
+            currentOptions,
+            p.loc
+          )
+        ) {
+          return true
         }
-        // TODO else if (
-        //   __COMPAT__ &&
-        //   checkCompatEnabled(
-        //     CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
-        //     context,
-        //     p.loc
-        //   )
-        // ) {
-        //   return true
-        // }
       }
+    } else if (
+      __COMPAT__ &&
+      // :is on plain element - only treat as component in compat mode
+      p.name === 'bind' &&
+      isStaticArgOf(p.arg, 'is') &&
+      checkCompatEnabled(
+        CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
+        currentOptions,
+        p.loc
+      )
+    ) {
+      return true
     }
-    // TODO else if (
-    //   __COMPAT__ &&
-    //   // :is on plain element - only treat as component in compat mode
-    //   p.name === 'bind' &&
-    //   isStaticArgOf(p.arg, 'is') &&
-    //   checkCompatEnabled(
-    //     CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
-    //     context,
-    //     p.loc
-    //   )
-    // ) {
-    //   return true
-    // }
   }
   return false
 }
@@ -818,7 +919,7 @@ function emitError(code: ErrorCodes, index: number) {
 
 function reset() {
   tokenizer.reset()
-  currentElement = null
+  currentOpenTag = null
   currentProp = null
   currentAttrValue = ''
   currentAttrStartIndex = -1
@@ -829,7 +930,17 @@ function reset() {
 export function baseParse(input: string, options?: ParserOptions): RootNode {
   reset()
   currentInput = input
-  currentOptions = extend({}, defaultParserOptions, options)
+  currentOptions = extend({}, defaultParserOptions)
+
+  if (options) {
+    let key: keyof ParserOptions
+    for (key in options) {
+      if (options[key] != null) {
+        // @ts-ignore
+        currentOptions[key] = options[key]
+      }
+    }
+  }
 
   if (__DEV__) {
     if (!__BROWSER__ && currentOptions.decodeEntities) {
index c20af972da2be6e0583348f6db3200859ae12f53..1f2f9ae247dbc32424b223e367b62d5f821b710c 100644 (file)
@@ -1,6 +1,6 @@
 import Vue from '@vue/compat'
 import { nextTick } from '@vue/runtime-core'
-import { CompilerDeprecationTypes } from '../../compiler-core/src'
+import { CompilerDeprecationTypes } from '@vue/compiler-core'
 import { toggleDeprecationWarning } from '../../runtime-core/src/compat/compatConfig'
 import { triggerEvent } from './utils'
 
@@ -81,16 +81,6 @@ test('COMPILER_V_BIND_SYNC', async () => {
   expect(CompilerDeprecationTypes.COMPILER_V_BIND_SYNC).toHaveBeenWarned()
 })
 
-test('COMPILER_V_BIND_PROP', () => {
-  const vm = new Vue({
-    template: `<div :id.prop="'foo'"/>`
-  }).$mount()
-
-  expect(vm.$el).toBeInstanceOf(HTMLDivElement)
-  expect(vm.$el.id).toBe('foo')
-  expect(CompilerDeprecationTypes.COMPILER_V_BIND_PROP).toHaveBeenWarned()
-})
-
 test('COMPILER_V_BIND_OBJECT_ORDER', () => {
   const vm = new Vue({
     template: `<div id="foo" v-bind="{ id: 'bar', class: 'baz' }" />`