]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(reactivity-transform): allow custom shorthands
authorAnthony Fu <anthonyfu117@hotmail.com>
Wed, 4 May 2022 00:58:21 +0000 (08:58 +0800)
committerAnthony Fu <anthonyfu117@hotmail.com>
Wed, 4 May 2022 00:58:21 +0000 (08:58 +0800)
packages/compiler-sfc/src/compileScript.ts
packages/compiler-sfc/src/index.ts
packages/reactivity-transform/README.md
packages/reactivity-transform/__tests__/reactivityTransform.spec.ts
packages/reactivity-transform/src/reactivityTransform.ts

index f70faeaf02b2956be67d8ab58aa33007b2c0f92e..35b613afbdeaa559a957416c373f24e65d8fd5e2 100644 (file)
@@ -13,7 +13,13 @@ import {
 } from '@vue/compiler-dom'
 import { SFCDescriptor, SFCScriptBlock } from './parse'
 import { parse as _parse, ParserOptions, ParserPlugin } from '@babel/parser'
-import { camelize, capitalize, generateCodeFrame, makeMap } from '@vue/shared'
+import {
+  camelize,
+  capitalize,
+  generateCodeFrame,
+  isObject,
+  makeMap
+} from '@vue/shared'
 import {
   Node,
   Declaration,
@@ -48,7 +54,10 @@ import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
 import { warnOnce } from './warn'
 import { rewriteDefault } from './rewriteDefault'
 import { createCache } from './cache'
-import { shouldTransform, transformAST } from '@vue/reactivity-transform'
+import {
+  createReactivityTransformer,
+  ReactivityTransformerOptions
+} from '@vue/reactivity-transform'
 
 // Special compiler macros
 const DEFINE_PROPS = 'defineProps'
@@ -85,7 +94,7 @@ export interface SFCScriptCompileOptions {
    * (Experimental) Enable syntax transform for using refs without `.value` and
    * using destructured props with reactivity
    */
-  reactivityTransform?: boolean
+  reactivityTransform?: boolean | ReactivityTransformerOptions
   /**
    * (Experimental) Enable syntax transform for using refs without `.value`
    * https://github.com/vuejs/rfcs/discussions/369
@@ -146,6 +155,13 @@ export function compileScript(
     !!options.refTransform
   const enablePropsTransform =
     !!options.reactivityTransform || !!options.propsDestructureTransform
+  const reactivityTransformer = enableReactivityTransform
+    ? createReactivityTransformer(
+        isObject(options.reactivityTransform)
+          ? options.reactivityTransform
+          : undefined
+      )
+    : undefined
   const isProd = !!options.isProd
   const genSourceMap = options.sourceMap !== false
   let refBindings: string[] | undefined
@@ -192,11 +208,11 @@ export function compileScript(
         sourceType: 'module'
       }).program
       const bindings = analyzeScriptBindings(scriptAst.body)
-      if (enableReactivityTransform && shouldTransform(content)) {
+      if (enableReactivityTransform && reactivityTransformer?.shouldTransform(content)) {
         const s = new MagicString(source)
         const startOffset = script.loc.start.offset
         const endOffset = script.loc.end.offset
-        const { importedHelpers } = transformAST(scriptAst, s, startOffset)
+        const { importedHelpers } = reactivityTransformer.transformAST(scriptAst, s, startOffset)
         if (importedHelpers.length) {
           s.prepend(
             `import { ${importedHelpers
@@ -864,8 +880,8 @@ export function compileScript(
     }
 
     // apply reactivity transform
-    if (enableReactivityTransform && shouldTransform(script.content)) {
-      const { rootRefs, importedHelpers } = transformAST(
+    if (enableReactivityTransform && reactivityTransformer?.shouldTransform(script.content)) {
+      const { rootRefs, importedHelpers } = reactivityTransformer.transformAST(
         scriptAst,
         s,
         scriptStartOffset!
@@ -1116,10 +1132,10 @@ export function compileScript(
   if (
     (enableReactivityTransform &&
       // normal <script> had ref bindings that maybe used in <script setup>
-      (refBindings || shouldTransform(scriptSetup.content))) ||
+      (refBindings || reactivityTransformer?.shouldTransform(scriptSetup.content))) ||
     propsDestructureDecl
   ) {
-    const { rootRefs, importedHelpers } = transformAST(
+    const { rootRefs, importedHelpers } = reactivityTransformer!.transformAST(
       scriptSetupAst,
       s,
       startOffset,
index bf89c5de45464562c9c2e128fca940c89817847c..fac40237ed1fd9456b9ac6e0a76c797632601101 100644 (file)
@@ -5,6 +5,7 @@ export { compileStyle, compileStyleAsync } from './compileStyle'
 export { compileScript } from './compileScript'
 export { rewriteDefault } from './rewriteDefault'
 export {
+  createReactivityTransformer,
   shouldTransform as shouldTransformRef,
   transform as transformRef,
   transformAST as transformRefAST
index 92277a8b288d44066b646640f9ba2fcd09673017..c16d4a9b0b2d0b18c0592db6257cea044f09863e 100644 (file)
@@ -119,3 +119,49 @@ const {
 
 console.log(s.toString()) // let a = _ref(0); a.value++
 ```
+
+### `createReactivityTransformer`
+
+Create a transformer with custom options.
+
+```js
+import { createReactivityTransformer } from '@vue/reactivity-transform'
+
+const transformer = createReactivityTransformer({
+  /* options */
+})
+
+transformer.shouldTransform(code)
+transformer.transform(code)
+```
+
+## Custom Shorthands
+
+To use custom $-prefixed shorthands, you can pass them as a list to `createReactivityTransformer`:
+
+```js
+import { createReactivityTransformer } from '@vue/reactivity-transform'
+
+const transformer = createReactivityTransformer({
+  // define custom shorthands
+  shorthands: ['useFoo', 'useBar']
+})
+
+transformer.transform(code)
+```
+
+Unlike built-in shorthands, custom shorthands are will NOT be auto imported.
+
+```ts
+const foo = $useFoo() // equivalent $(useFoo())
+console.log(foo)
+```
+
+will be transformed to
+
+```ts
+const foo = useFoo()
+console.log(foo.value)
+```
+
+The importing and TypeScript definitions should be handled by upper-level tooling.
index ece6e3512f91f508a77d10f43bbe62cc0c40c2e9..0953a54332f35065fc2262ad4ffc314f3a555357 100644 (file)
@@ -1,5 +1,5 @@
 import { parse } from '@babel/parser'
-import { transform } from '../src'
+import { createReactivityTransformer, transform } from '../src'
 
 function assertCode(code: string) {
   // parse the generated code to make sure it is valid
@@ -459,3 +459,22 @@ describe('errors', () => {
     )
   })
 })
+
+test('custom shorthands', () => {
+  const transformer = createReactivityTransformer({
+    shorthands: ['useFoo', 'useBar']
+  })
+
+  const { code } = transformer.transform(
+    `
+    const foo = $useFoo()
+    const { bar } = $useBar()
+    console.log(foo, bar)
+    `
+  )
+
+  expect(code).toMatch(`const foo = useFoo()`)
+  expect(code).toMatch(`const __$temp_1 = useBar()`)
+  expect(code).toMatch(`bar = _toRef(__$temp_1, 'bar')`)
+  expect(code).toMatch(`console.log(foo.value, bar.value)`)
+})
index 809e3e4e246b2b6205f90de8199bb8c8dbf1ca3c..6f97367eb092237dd4b86a686b8affdc05845ebf 100644 (file)
@@ -25,12 +25,13 @@ import { hasOwn, isArray, isString } from '@vue/shared'
 
 const CONVERT_SYMBOL = '$'
 const ESCAPE_SYMBOL = '$$'
-const shorthands = ['ref', 'computed', 'shallowRef', 'toRef', 'customRef']
-const transformCheckRE = /[^\w]\$(?:\$|ref|computed|shallowRef)?\s*(\(|\<)/
-
-export function shouldTransform(src: string): boolean {
-  return transformCheckRE.test(src)
-}
+const builtInShorthands = [
+  'ref',
+  'computed',
+  'shallowRef',
+  'toRef',
+  'customRef'
+]
 
 type Scope = Record<string, boolean | 'prop'>
 
@@ -48,623 +49,665 @@ export interface RefTransformResults {
   importedHelpers: string[]
 }
 
-export function transform(
-  src: string,
-  {
-    filename,
-    sourceMap,
-    parserPlugins,
-    importHelpersFrom = 'vue'
-  }: RefTransformOptions = {}
-): RefTransformResults {
-  const plugins: ParserPlugin[] = parserPlugins || []
-  if (filename) {
-    if (/\.tsx?$/.test(filename)) {
-      plugins.push('typescript')
-    }
-    if (filename.endsWith('x')) {
-      plugins.push('jsx')
-    }
-  }
+export interface ReactivityTransformerOptions {
+  shorthands?: string[]
+}
 
-  const ast = parse(src, {
-    sourceType: 'module',
-    plugins
-  })
-  const s = new MagicString(src)
-  const res = transformAST(ast.program, s, 0)
-
-  // inject helper imports
-  if (res.importedHelpers.length) {
-    s.prepend(
-      `import { ${res.importedHelpers
-        .map(h => `${h} as _${h}`)
-        .join(', ')} } from '${importHelpersFrom}'\n`
-    )
-  }
+export function createReactivityTransformer(
+  options: ReactivityTransformerOptions = {}
+) {
+  const shorthands = [...builtInShorthands, ...(options.shorthands || [])]
+  const transformCheckRE = new RegExp(
+    `[^\\w]\\$(?:\$|${shorthands.map(escapeRegExp).join('|')})?\\s*(\\(|\\<)`
+  )
 
-  return {
-    ...res,
-    code: s.toString(),
-    map: sourceMap
-      ? s.generateMap({
-          source: filename,
-          hires: true,
-          includeContent: true
-        })
-      : null
+  function shouldTransform(src: string): boolean {
+    return transformCheckRE.test(src)
   }
-}
 
-export function transformAST(
-  ast: Program,
-  s: MagicString,
-  offset = 0,
-  knownRefs?: string[],
-  knownProps?: Record<
-    string, // public prop key
+  function transform(
+    src: string,
     {
-      local: string // local identifier, may be different
-      default?: any
+      filename,
+      sourceMap,
+      parserPlugins,
+      importHelpersFrom = 'vue'
+    }: RefTransformOptions = {}
+  ): RefTransformResults {
+    const plugins: ParserPlugin[] = parserPlugins || []
+    if (filename) {
+      if (/\.tsx?$/.test(filename)) {
+        plugins.push('typescript')
+      }
+      if (filename.endsWith('x')) {
+        plugins.push('jsx')
+      }
     }
-  >
-): {
-  rootRefs: string[]
-  importedHelpers: string[]
-} {
-  // TODO remove when out of experimental
-  warnExperimental()
-
-  let convertSymbol = CONVERT_SYMBOL
-  let escapeSymbol = ESCAPE_SYMBOL
-
-  // macro import handling
-  for (const node of ast.body) {
-    if (
-      node.type === 'ImportDeclaration' &&
-      node.source.value === 'vue/macros'
-    ) {
-      // remove macro imports
-      s.remove(node.start! + offset, node.end! + offset)
-      // check aliasing
-      for (const specifier of node.specifiers) {
-        if (specifier.type === 'ImportSpecifier') {
-          const imported = (specifier.imported as Identifier).name
-          const local = specifier.local.name
-          if (local !== imported) {
-            if (imported === ESCAPE_SYMBOL) {
-              escapeSymbol = local
-            } else if (imported === CONVERT_SYMBOL) {
-              convertSymbol = local
-            } else {
-              error(
-                `macro imports for ref-creating methods do not support aliasing.`,
-                specifier
-              )
+
+    const ast = parse(src, {
+      sourceType: 'module',
+      plugins
+    })
+    const s = new MagicString(src)
+    const res = transformAST(ast.program, s, 0)
+
+    // inject helper imports
+    if (res.importedHelpers.length) {
+      s.prepend(
+        `import { ${res.importedHelpers
+          .map(h => `${h} as _${h}`)
+          .join(', ')} } from '${importHelpersFrom}'\n`
+      )
+    }
+
+    return {
+      ...res,
+      code: s.toString(),
+      map: sourceMap
+        ? s.generateMap({
+            source: filename,
+            hires: true,
+            includeContent: true
+          })
+        : null
+    }
+  }
+
+  function transformAST(
+    ast: Program,
+    s: MagicString,
+    offset = 0,
+    knownRefs?: string[],
+    knownProps?: Record<
+      string, // public prop key
+      {
+        local: string // local identifier, may be different
+        default?: any
+      }
+    >
+  ): {
+    rootRefs: string[]
+    importedHelpers: string[]
+  } {
+    // TODO remove when out of experimental
+    warnExperimental()
+
+    let convertSymbol = CONVERT_SYMBOL
+    let escapeSymbol = ESCAPE_SYMBOL
+
+    // macro import handling
+    for (const node of ast.body) {
+      if (
+        node.type === 'ImportDeclaration' &&
+        node.source.value === 'vue/macros'
+      ) {
+        // remove macro imports
+        s.remove(node.start! + offset, node.end! + offset)
+        // check aliasing
+        for (const specifier of node.specifiers) {
+          if (specifier.type === 'ImportSpecifier') {
+            const imported = (specifier.imported as Identifier).name
+            const local = specifier.local.name
+            if (local !== imported) {
+              if (imported === ESCAPE_SYMBOL) {
+                escapeSymbol = local
+              } else if (imported === CONVERT_SYMBOL) {
+                convertSymbol = local
+              } else {
+                error(
+                  `macro imports for ref-creating methods do not support aliasing.`,
+                  specifier
+                )
+              }
             }
           }
         }
       }
     }
-  }
 
-  const importedHelpers = new Set<string>()
-  const rootScope: Scope = {}
-  const scopeStack: Scope[] = [rootScope]
-  let currentScope: Scope = rootScope
-  let escapeScope: CallExpression | undefined // inside $$()
-  const excludedIds = new WeakSet<Identifier>()
-  const parentStack: Node[] = []
-  const propsLocalToPublicMap = Object.create(null)
-
-  if (knownRefs) {
-    for (const key of knownRefs) {
-      rootScope[key] = true
+    const importedHelpers = new Set<string>()
+    const rootScope: Scope = {}
+    const scopeStack: Scope[] = [rootScope]
+    let currentScope: Scope = rootScope
+    let escapeScope: CallExpression | undefined // inside $$()
+    const excludedIds = new WeakSet<Identifier>()
+    const parentStack: Node[] = []
+    const propsLocalToPublicMap = Object.create(null)
+
+    if (knownRefs) {
+      for (const key of knownRefs) {
+        rootScope[key] = true
+      }
     }
-  }
-  if (knownProps) {
-    for (const key in knownProps) {
-      const { local } = knownProps[key]
-      rootScope[local] = 'prop'
-      propsLocalToPublicMap[local] = key
+    if (knownProps) {
+      for (const key in knownProps) {
+        const { local } = knownProps[key]
+        rootScope[local] = 'prop'
+        propsLocalToPublicMap[local] = key
+      }
     }
-  }
 
-  function isRefCreationCall(callee: string): string | false {
-    if (callee === convertSymbol) {
-      return convertSymbol
-    }
-    if (callee[0] === '$' && shorthands.includes(callee.slice(1))) {
-      return callee
+    function isRefCreationCall(callee: string): string | false {
+      if (callee === convertSymbol) {
+        return convertSymbol
+      }
+      if (callee[0] === '$' && shorthands.includes(callee.slice(1))) {
+        return callee
+      }
+      return false
     }
-    return false
-  }
 
-  function error(msg: string, node: Node) {
-    const e = new Error(msg)
-    ;(e as any).node = node
-    throw e
-  }
+    function error(msg: string, node: Node) {
+      const e = new Error(msg)
+      ;(e as any).node = node
+      throw e
+    }
 
-  function helper(msg: string) {
-    importedHelpers.add(msg)
-    return `_${msg}`
-  }
+    function helper(name: string) {
+      importedHelpers.add(name)
+      return `_${name}`
+    }
 
-  function registerBinding(id: Identifier, isRef = false) {
-    excludedIds.add(id)
-    if (currentScope) {
-      currentScope[id.name] = isRef
-    } else {
-      error(
-        'registerBinding called without active scope, something is wrong.',
-        id
-      )
+    function registerBinding(id: Identifier, isRef = false) {
+      excludedIds.add(id)
+      if (currentScope) {
+        currentScope[id.name] = isRef
+      } else {
+        error(
+          'registerBinding called without active scope, something is wrong.',
+          id
+        )
+      }
     }
-  }
 
-  const registerRefBinding = (id: Identifier) => registerBinding(id, true)
+    const registerRefBinding = (id: Identifier) => registerBinding(id, true)
 
-  let tempVarCount = 0
-  function genTempVar() {
-    return `__$temp_${++tempVarCount}`
-  }
+    let tempVarCount = 0
+    function genTempVar() {
+      return `__$temp_${++tempVarCount}`
+    }
 
-  function snip(node: Node) {
-    return s.original.slice(node.start! + offset, node.end! + offset)
-  }
+    function snip(node: Node) {
+      return s.original.slice(node.start! + offset, node.end! + offset)
+    }
 
-  function walkScope(node: Program | BlockStatement, isRoot = false) {
-    for (const stmt of node.body) {
-      if (stmt.type === 'VariableDeclaration') {
-        walkVariableDeclaration(stmt, isRoot)
-      } else if (
-        stmt.type === 'FunctionDeclaration' ||
-        stmt.type === 'ClassDeclaration'
-      ) {
-        if (stmt.declare || !stmt.id) continue
-        registerBinding(stmt.id)
-      } else if (
-        (stmt.type === 'ForOfStatement' || stmt.type === 'ForInStatement') &&
-        stmt.left.type === 'VariableDeclaration'
-      ) {
-        walkVariableDeclaration(stmt.left)
-      } else if (
-        stmt.type === 'ExportNamedDeclaration' &&
-        stmt.declaration &&
-        stmt.declaration.type === 'VariableDeclaration'
-      ) {
-        walkVariableDeclaration(stmt.declaration, isRoot)
-      } else if (
-        stmt.type === 'LabeledStatement' &&
-        stmt.body.type === 'VariableDeclaration'
-      ) {
-        walkVariableDeclaration(stmt.body, isRoot)
+    function walkScope(node: Program | BlockStatement, isRoot = false) {
+      for (const stmt of node.body) {
+        if (stmt.type === 'VariableDeclaration') {
+          walkVariableDeclaration(stmt, isRoot)
+        } else if (
+          stmt.type === 'FunctionDeclaration' ||
+          stmt.type === 'ClassDeclaration'
+        ) {
+          if (stmt.declare || !stmt.id) continue
+          registerBinding(stmt.id)
+        } else if (
+          (stmt.type === 'ForOfStatement' || stmt.type === 'ForInStatement') &&
+          stmt.left.type === 'VariableDeclaration'
+        ) {
+          walkVariableDeclaration(stmt.left)
+        } else if (
+          stmt.type === 'ExportNamedDeclaration' &&
+          stmt.declaration &&
+          stmt.declaration.type === 'VariableDeclaration'
+        ) {
+          walkVariableDeclaration(stmt.declaration, isRoot)
+        } else if (
+          stmt.type === 'LabeledStatement' &&
+          stmt.body.type === 'VariableDeclaration'
+        ) {
+          walkVariableDeclaration(stmt.body, isRoot)
+        }
       }
     }
-  }
 
-  function walkVariableDeclaration(stmt: VariableDeclaration, isRoot = false) {
-    if (stmt.declare) {
-      return
-    }
-    for (const decl of stmt.declarations) {
-      let refCall
-      const isCall =
-        decl.init &&
-        decl.init.type === 'CallExpression' &&
-        decl.init.callee.type === 'Identifier'
-      if (
-        isCall &&
-        (refCall = isRefCreationCall((decl as any).init.callee.name))
-      ) {
-        processRefDeclaration(refCall, decl.id, decl.init as CallExpression)
-      } else {
-        const isProps =
-          isRoot && isCall && (decl as any).init.callee.name === 'defineProps'
-        for (const id of extractIdentifiers(decl.id)) {
-          if (isProps) {
-            // for defineProps destructure, only exclude them since they
-            // are already passed in as knownProps
-            excludedIds.add(id)
-          } else {
-            registerBinding(id)
+    function walkVariableDeclaration(
+      stmt: VariableDeclaration,
+      isRoot = false
+    ) {
+      if (stmt.declare) {
+        return
+      }
+      for (const decl of stmt.declarations) {
+        let refCall
+        const isCall =
+          decl.init &&
+          decl.init.type === 'CallExpression' &&
+          decl.init.callee.type === 'Identifier'
+        if (
+          isCall &&
+          (refCall = isRefCreationCall((decl as any).init.callee.name))
+        ) {
+          processRefDeclaration(refCall, decl.id, decl.init as CallExpression)
+        } else {
+          const isProps =
+            isRoot && isCall && (decl as any).init.callee.name === 'defineProps'
+          for (const id of extractIdentifiers(decl.id)) {
+            if (isProps) {
+              // for defineProps destructure, only exclude them since they
+              // are already passed in as knownProps
+              excludedIds.add(id)
+            } else {
+              registerBinding(id)
+            }
           }
         }
       }
     }
-  }
 
-  function processRefDeclaration(
-    method: string,
-    id: VariableDeclarator['id'],
-    call: CallExpression
-  ) {
-    excludedIds.add(call.callee as Identifier)
-    if (method === convertSymbol) {
-      // $
-      // remove macro
-      s.remove(call.callee.start! + offset, call.callee.end! + offset)
-      if (id.type === 'Identifier') {
-        // single variable
-        registerRefBinding(id)
-      } else if (id.type === 'ObjectPattern') {
-        processRefObjectPattern(id, call)
-      } else if (id.type === 'ArrayPattern') {
-        processRefArrayPattern(id, call)
-      }
-    } else {
-      // shorthands
-      if (id.type === 'Identifier') {
-        registerRefBinding(id)
-        // replace call
-        s.overwrite(
-          call.start! + offset,
-          call.start! + method.length + offset,
-          helper(method.slice(1))
-        )
+    function processRefDeclaration(
+      method: string,
+      id: VariableDeclarator['id'],
+      call: CallExpression
+    ) {
+      excludedIds.add(call.callee as Identifier)
+      if (method === convertSymbol) {
+        // $
+        // remove macro
+        s.remove(call.callee.start! + offset, call.callee.end! + offset)
+        if (id.type === 'Identifier') {
+          // single variable
+          registerRefBinding(id)
+        } else if (id.type === 'ObjectPattern') {
+          processRefObjectPattern(id, call)
+        } else if (id.type === 'ArrayPattern') {
+          processRefArrayPattern(id, call)
+        }
       } else {
-        error(`${method}() cannot be used with destructure patterns.`, call)
+        // shorthands
+        const name = method.slice(1)
+        const isBuiltIn = builtInShorthands.includes(name)
+        if (id.type !== 'Identifier' && isBuiltIn) {
+          error(`${method}() cannot be used with destructure patterns.`, call)
+        } else {
+          // replace call
+          s.overwrite(
+            call.start! + offset,
+            call.start! + method.length + offset,
+            isBuiltIn ? helper(name) : name
+          )
+          if (id.type === 'Identifier') {
+            // single variable
+            registerRefBinding(id)
+          } else if (id.type === 'ObjectPattern') {
+            processRefObjectPattern(id, call)
+          } else if (id.type === 'ArrayPattern') {
+            processRefArrayPattern(id, call)
+          }
+        }
       }
     }
-  }
 
-  function processRefObjectPattern(
-    pattern: ObjectPattern,
-    call: CallExpression,
-    tempVar?: string,
-    path: PathSegment[] = []
-  ) {
-    if (!tempVar) {
-      tempVar = genTempVar()
-      // const { x } = $(useFoo()) --> const __$temp_1 = useFoo()
-      s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
-    }
+    function processRefObjectPattern(
+      pattern: ObjectPattern,
+      call: CallExpression,
+      tempVar?: string,
+      path: PathSegment[] = []
+    ) {
+      if (!tempVar) {
+        tempVar = genTempVar()
+        // const { x } = $(useFoo()) --> const __$temp_1 = useFoo()
+        s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
+      }
 
-    for (const p of pattern.properties) {
-      let nameId: Identifier | undefined
-      let key: Expression | string | undefined
-      let defaultValue: Expression | undefined
-      if (p.type === 'ObjectProperty') {
-        if (p.key.start! === p.value.start!) {
-          // shorthand { foo }
-          nameId = p.key as Identifier
-          if (p.value.type === 'Identifier') {
-            // avoid shorthand value identifier from being processed
-            excludedIds.add(p.value)
-          } else if (
-            p.value.type === 'AssignmentPattern' &&
-            p.value.left.type === 'Identifier'
-          ) {
-            // { foo = 1 }
-            excludedIds.add(p.value.left)
-            defaultValue = p.value.right
-          }
-        } else {
-          key = p.computed ? p.key : (p.key as Identifier).name
-          if (p.value.type === 'Identifier') {
-            // { foo: bar }
-            nameId = p.value
-          } else if (p.value.type === 'ObjectPattern') {
-            processRefObjectPattern(p.value, call, tempVar, [...path, key])
-          } else if (p.value.type === 'ArrayPattern') {
-            processRefArrayPattern(p.value, call, tempVar, [...path, key])
-          } else if (p.value.type === 'AssignmentPattern') {
-            if (p.value.left.type === 'Identifier') {
-              // { foo: bar = 1 }
-              nameId = p.value.left
+      for (const p of pattern.properties) {
+        let nameId: Identifier | undefined
+        let key: Expression | string | undefined
+        let defaultValue: Expression | undefined
+        if (p.type === 'ObjectProperty') {
+          if (p.key.start! === p.value.start!) {
+            // shorthand { foo }
+            nameId = p.key as Identifier
+            if (p.value.type === 'Identifier') {
+              // avoid shorthand value identifier from being processed
+              excludedIds.add(p.value)
+            } else if (
+              p.value.type === 'AssignmentPattern' &&
+              p.value.left.type === 'Identifier'
+            ) {
+              // { foo = 1 }
+              excludedIds.add(p.value.left)
               defaultValue = p.value.right
-            } else if (p.value.left.type === 'ObjectPattern') {
-              processRefObjectPattern(p.value.left, call, tempVar, [
-                ...path,
-                [key, p.value.right]
-              ])
-            } else if (p.value.left.type === 'ArrayPattern') {
-              processRefArrayPattern(p.value.left, call, tempVar, [
-                ...path,
-                [key, p.value.right]
-              ])
-            } else {
-              // MemberExpression case is not possible here, ignore
+            }
+          } else {
+            key = p.computed ? p.key : (p.key as Identifier).name
+            if (p.value.type === 'Identifier') {
+              // { foo: bar }
+              nameId = p.value
+            } else if (p.value.type === 'ObjectPattern') {
+              processRefObjectPattern(p.value, call, tempVar, [...path, key])
+            } else if (p.value.type === 'ArrayPattern') {
+              processRefArrayPattern(p.value, call, tempVar, [...path, key])
+            } else if (p.value.type === 'AssignmentPattern') {
+              if (p.value.left.type === 'Identifier') {
+                // { foo: bar = 1 }
+                nameId = p.value.left
+                defaultValue = p.value.right
+              } else if (p.value.left.type === 'ObjectPattern') {
+                processRefObjectPattern(p.value.left, call, tempVar, [
+                  ...path,
+                  [key, p.value.right]
+                ])
+              } else if (p.value.left.type === 'ArrayPattern') {
+                processRefArrayPattern(p.value.left, call, tempVar, [
+                  ...path,
+                  [key, p.value.right]
+                ])
+              } else {
+                // MemberExpression case is not possible here, ignore
+              }
             }
           }
+        } else {
+          // rest element { ...foo }
+          error(`reactivity destructure does not support rest elements.`, p)
+        }
+        if (nameId) {
+          registerRefBinding(nameId)
+          // inject toRef() after original replaced pattern
+          const source = pathToString(tempVar, path)
+          const keyStr = isString(key)
+            ? `'${key}'`
+            : key
+            ? snip(key)
+            : `'${nameId.name}'`
+          const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
+          s.appendLeft(
+            call.end! + offset,
+            `,\n  ${nameId.name} = ${helper(
+              'toRef'
+            )}(${source}, ${keyStr}${defaultStr})`
+          )
         }
-      } else {
-        // rest element { ...foo }
-        error(`reactivity destructure does not support rest elements.`, p)
-      }
-      if (nameId) {
-        registerRefBinding(nameId)
-        // inject toRef() after original replaced pattern
-        const source = pathToString(tempVar, path)
-        const keyStr = isString(key)
-          ? `'${key}'`
-          : key
-          ? snip(key)
-          : `'${nameId.name}'`
-        const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
-        s.appendLeft(
-          call.end! + offset,
-          `,\n  ${nameId.name} = ${helper(
-            'toRef'
-          )}(${source}, ${keyStr}${defaultStr})`
-        )
       }
     }
-  }
-
-  function processRefArrayPattern(
-    pattern: ArrayPattern,
-    call: CallExpression,
-    tempVar?: string,
-    path: PathSegment[] = []
-  ) {
-    if (!tempVar) {
-      // const [x] = $(useFoo()) --> const __$temp_1 = useFoo()
-      tempVar = genTempVar()
-      s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
-    }
 
-    for (let i = 0; i < pattern.elements.length; i++) {
-      const e = pattern.elements[i]
-      if (!e) continue
-      let nameId: Identifier | undefined
-      let defaultValue: Expression | undefined
-      if (e.type === 'Identifier') {
-        // [a] --> [__a]
-        nameId = e
-      } else if (e.type === 'AssignmentPattern') {
-        // [a = 1]
-        nameId = e.left as Identifier
-        defaultValue = e.right
-      } else if (e.type === 'RestElement') {
-        // [...a]
-        error(`reactivity destructure does not support rest elements.`, e)
-      } else if (e.type === 'ObjectPattern') {
-        processRefObjectPattern(e, call, tempVar, [...path, i])
-      } else if (e.type === 'ArrayPattern') {
-        processRefArrayPattern(e, call, tempVar, [...path, i])
+    function processRefArrayPattern(
+      pattern: ArrayPattern,
+      call: CallExpression,
+      tempVar?: string,
+      path: PathSegment[] = []
+    ) {
+      if (!tempVar) {
+        // const [x] = $(useFoo()) --> const __$temp_1 = useFoo()
+        tempVar = genTempVar()
+        s.overwrite(pattern.start! + offset, pattern.end! + offset, tempVar)
       }
-      if (nameId) {
-        registerRefBinding(nameId)
-        // inject toRef() after original replaced pattern
-        const source = pathToString(tempVar, path)
-        const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
-        s.appendLeft(
-          call.end! + offset,
-          `,\n  ${nameId.name} = ${helper(
-            'toRef'
-          )}(${source}, ${i}${defaultStr})`
-        )
+
+      for (let i = 0; i < pattern.elements.length; i++) {
+        const e = pattern.elements[i]
+        if (!e) continue
+        let nameId: Identifier | undefined
+        let defaultValue: Expression | undefined
+        if (e.type === 'Identifier') {
+          // [a] --> [__a]
+          nameId = e
+        } else if (e.type === 'AssignmentPattern') {
+          // [a = 1]
+          nameId = e.left as Identifier
+          defaultValue = e.right
+        } else if (e.type === 'RestElement') {
+          // [...a]
+          error(`reactivity destructure does not support rest elements.`, e)
+        } else if (e.type === 'ObjectPattern') {
+          processRefObjectPattern(e, call, tempVar, [...path, i])
+        } else if (e.type === 'ArrayPattern') {
+          processRefArrayPattern(e, call, tempVar, [...path, i])
+        }
+        if (nameId) {
+          registerRefBinding(nameId)
+          // inject toRef() after original replaced pattern
+          const source = pathToString(tempVar, path)
+          const defaultStr = defaultValue ? `, ${snip(defaultValue)}` : ``
+          s.appendLeft(
+            call.end! + offset,
+            `,\n  ${nameId.name} = ${helper(
+              'toRef'
+            )}(${source}, ${i}${defaultStr})`
+          )
+        }
       }
     }
-  }
 
-  type PathSegmentAtom = Expression | string | number
+    type PathSegmentAtom = Expression | string | number
 
-  type PathSegment =
-    | PathSegmentAtom
-    | [PathSegmentAtom, Expression /* default value */]
+    type PathSegment =
+      | PathSegmentAtom
+      | [PathSegmentAtom, Expression /* default value */]
 
-  function pathToString(source: string, path: PathSegment[]): string {
-    if (path.length) {
-      for (const seg of path) {
-        if (isArray(seg)) {
-          source = `(${source}${segToString(seg[0])} || ${snip(seg[1])})`
-        } else {
-          source += segToString(seg)
+    function pathToString(source: string, path: PathSegment[]): string {
+      if (path.length) {
+        for (const seg of path) {
+          if (isArray(seg)) {
+            source = `(${source}${segToString(seg[0])} || ${snip(seg[1])})`
+          } else {
+            source += segToString(seg)
+          }
         }
       }
+      return source
     }
-    return source
-  }
 
-  function segToString(seg: PathSegmentAtom): string {
-    if (typeof seg === 'number') {
-      return `[${seg}]`
-    } else if (typeof seg === 'string') {
-      return `.${seg}`
-    } else {
-      return snip(seg)
+    function segToString(seg: PathSegmentAtom): string {
+      if (typeof seg === 'number') {
+        return `[${seg}]`
+      } else if (typeof seg === 'string') {
+        return `.${seg}`
+      } else {
+        return snip(seg)
+      }
     }
-  }
 
-  function rewriteId(
-    scope: Scope,
-    id: Identifier,
-    parent: Node,
-    parentStack: Node[]
-  ): boolean {
-    if (hasOwn(scope, id.name)) {
-      const bindingType = scope[id.name]
-      if (bindingType) {
-        const isProp = bindingType === 'prop'
-        if (isStaticProperty(parent) && parent.shorthand) {
-          // let binding used in a property shorthand
-          // skip for destructure patterns
-          if (
-            !(parent as any).inPattern ||
-            isInDestructureAssignment(parent, parentStack)
-          ) {
+    function rewriteId(
+      scope: Scope,
+      id: Identifier,
+      parent: Node,
+      parentStack: Node[]
+    ): boolean {
+      if (hasOwn(scope, id.name)) {
+        const bindingType = scope[id.name]
+        if (bindingType) {
+          const isProp = bindingType === 'prop'
+          if (isStaticProperty(parent) && parent.shorthand) {
+            // let binding used in a property shorthand
+            // skip for destructure patterns
+            if (
+              !(parent as any).inPattern ||
+              isInDestructureAssignment(parent, parentStack)
+            ) {
+              if (isProp) {
+                if (escapeScope) {
+                  // prop binding in $$()
+                  // { prop } -> { prop: __prop_prop }
+                  registerEscapedPropBinding(id)
+                  s.appendLeft(
+                    id.end! + offset,
+                    `: __props_${propsLocalToPublicMap[id.name]}`
+                  )
+                } else {
+                  // { prop } -> { prop: __prop.prop }
+                  s.appendLeft(
+                    id.end! + offset,
+                    `: __props.${propsLocalToPublicMap[id.name]}`
+                  )
+                }
+              } else {
+                // { foo } -> { foo: foo.value }
+                s.appendLeft(id.end! + offset, `: ${id.name}.value`)
+              }
+            }
+          } else {
             if (isProp) {
               if (escapeScope) {
-                // prop binding in $$()
-                // { prop } -> { prop: __prop_prop }
+                // x --> __props_x
                 registerEscapedPropBinding(id)
-                s.appendLeft(
+                s.overwrite(
+                  id.start! + offset,
                   id.end! + offset,
-                  `__props_${propsLocalToPublicMap[id.name]}`
+                  `__props_${propsLocalToPublicMap[id.name]}`
                 )
               } else {
-                // { prop } -> { prop: __prop.prop }
-                s.appendLeft(
+                // x --> __props.x
+                s.overwrite(
+                  id.start! + offset,
                   id.end! + offset,
-                  `__props.${propsLocalToPublicMap[id.name]}`
+                  `__props.${propsLocalToPublicMap[id.name]}`
                 )
               }
             } else {
-              // { foo } -> { foo: foo.value }
-              s.appendLeft(id.end! + offset, `: ${id.name}.value`)
-            }
-          }
-        } else {
-          if (isProp) {
-            if (escapeScope) {
-              // x --> __props_x
-              registerEscapedPropBinding(id)
-              s.overwrite(
-                id.start! + offset,
-                id.end! + offset,
-                `__props_${propsLocalToPublicMap[id.name]}`
-              )
-            } else {
-              // x --> __props.x
-              s.overwrite(
-                id.start! + offset,
-                id.end! + offset,
-                `__props.${propsLocalToPublicMap[id.name]}`
-              )
+              // x --> x.value
+              s.appendLeft(id.end! + offset, '.value')
             }
-          } else {
-            // x --> x.value
-            s.appendLeft(id.end! + offset, '.value')
           }
         }
+        return true
       }
-      return true
+      return false
     }
-    return false
-  }
 
-  const propBindingRefs: Record<string, true> = {}
-  function registerEscapedPropBinding(id: Identifier) {
-    if (!propBindingRefs.hasOwnProperty(id.name)) {
-      propBindingRefs[id.name] = true
-      const publicKey = propsLocalToPublicMap[id.name]
-      s.prependRight(
-        offset,
-        `const __props_${publicKey} = ${helper(
-          `toRef`
-        )}(__props, '${publicKey}')\n`
-      )
+    const propBindingRefs: Record<string, true> = {}
+    function registerEscapedPropBinding(id: Identifier) {
+      if (!propBindingRefs.hasOwnProperty(id.name)) {
+        propBindingRefs[id.name] = true
+        const publicKey = propsLocalToPublicMap[id.name]
+        s.prependRight(
+          offset,
+          `const __props_${publicKey} = ${helper(
+            `toRef`
+          )}(__props, '${publicKey}')\n`
+        )
+      }
     }
-  }
 
-  // check root scope first
-  walkScope(ast, true)
-  ;(walk as any)(ast, {
-    enter(node: Node, parent?: Node) {
-      parent && parentStack.push(parent)
-
-      // function scopes
-      if (isFunctionType(node)) {
-        scopeStack.push((currentScope = {}))
-        walkFunctionParams(node, registerBinding)
-        if (node.body.type === 'BlockStatement') {
-          walkScope(node.body)
+    // check root scope first
+    walkScope(ast, true)
+    ;(walk as any)(ast, {
+      enter(node: Node, parent?: Node) {
+        parent && parentStack.push(parent)
+
+        // function scopes
+        if (isFunctionType(node)) {
+          scopeStack.push((currentScope = {}))
+          walkFunctionParams(node, registerBinding)
+          if (node.body.type === 'BlockStatement') {
+            walkScope(node.body)
+          }
+          return
         }
-        return
-      }
 
-      // catch param
-      if (node.type === 'CatchClause') {
-        scopeStack.push((currentScope = {}))
-        if (node.param && node.param.type === 'Identifier') {
-          registerBinding(node.param)
+        // catch param
+        if (node.type === 'CatchClause') {
+          scopeStack.push((currentScope = {}))
+          if (node.param && node.param.type === 'Identifier') {
+            registerBinding(node.param)
+          }
+          walkScope(node.body)
+          return
         }
-        walkScope(node.body)
-        return
-      }
 
-      // non-function block scopes
-      if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
-        scopeStack.push((currentScope = {}))
-        walkScope(node)
-        return
-      }
+        // non-function block scopes
+        if (node.type === 'BlockStatement' && !isFunctionType(parent!)) {
+          scopeStack.push((currentScope = {}))
+          walkScope(node)
+          return
+        }
 
-      // skip type nodes
-      if (
-        parent &&
-        parent.type.startsWith('TS') &&
-        parent.type !== 'TSAsExpression' &&
-        parent.type !== 'TSNonNullExpression' &&
-        parent.type !== 'TSTypeAssertion'
-      ) {
-        return this.skip()
-      }
+        // skip type nodes
+        if (
+          parent &&
+          parent.type.startsWith('TS') &&
+          parent.type !== 'TSAsExpression' &&
+          parent.type !== 'TSNonNullExpression' &&
+          parent.type !== 'TSTypeAssertion'
+        ) {
+          return this.skip()
+        }
 
-      if (
-        node.type === 'Identifier' &&
-        // if inside $$(), skip unless this is a destructured prop binding
-        !(escapeScope && rootScope[node.name] !== 'prop') &&
-        isReferencedIdentifier(node, parent!, parentStack) &&
-        !excludedIds.has(node)
-      ) {
-        // walk up the scope chain to check if id should be appended .value
-        let i = scopeStack.length
-        while (i--) {
-          if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
-            return
+        if (
+          node.type === 'Identifier' &&
+          // if inside $$(), skip unless this is a destructured prop binding
+          !(escapeScope && rootScope[node.name] !== 'prop') &&
+          isReferencedIdentifier(node, parent!, parentStack) &&
+          !excludedIds.has(node)
+        ) {
+          // walk up the scope chain to check if id should be appended .value
+          let i = scopeStack.length
+          while (i--) {
+            if (rewriteId(scopeStack[i], node, parent!, parentStack)) {
+              return
+            }
           }
         }
-      }
 
-      if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
-        const callee = node.callee.name
+        if (
+          node.type === 'CallExpression' &&
+          node.callee.type === 'Identifier'
+        ) {
+          const callee = node.callee.name
+
+          const refCall = isRefCreationCall(callee)
+          if (refCall && (!parent || parent.type !== 'VariableDeclarator')) {
+            return error(
+              `${refCall} can only be used as the initializer of ` +
+                `a variable declaration.`,
+              node
+            )
+          }
 
-        const refCall = isRefCreationCall(callee)
-        if (refCall && (!parent || parent.type !== 'VariableDeclarator')) {
-          return error(
-            `${refCall} can only be used as the initializer of ` +
-              `a variable declaration.`,
-            node
-          )
-        }
+          if (callee === escapeSymbol) {
+            s.remove(node.callee.start! + offset, node.callee.end! + offset)
+            escapeScope = node
+          }
 
-        if (callee === escapeSymbol) {
-          s.remove(node.callee.start! + offset, node.callee.end! + offset)
-          escapeScope = node
+          // TODO remove when out of experimental
+          if (callee === '$raw') {
+            error(
+              `$raw() has been replaced by $$(). ` +
+                `See ${RFC_LINK} for latest updates.`,
+              node
+            )
+          }
+          if (callee === '$fromRef') {
+            error(
+              `$fromRef() has been replaced by $(). ` +
+                `See ${RFC_LINK} for latest updates.`,
+              node
+            )
+          }
         }
-
-        // TODO remove when out of experimental
-        if (callee === '$raw') {
-          error(
-            `$raw() has been replaced by $$(). ` +
-              `See ${RFC_LINK} for latest updates.`,
-            node
-          )
+      },
+      leave(node: Node, parent?: Node) {
+        parent && parentStack.pop()
+        if (
+          (node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
+          isFunctionType(node)
+        ) {
+          scopeStack.pop()
+          currentScope = scopeStack[scopeStack.length - 1] || null
         }
-        if (callee === '$fromRef') {
-          error(
-            `$fromRef() has been replaced by $(). ` +
-              `See ${RFC_LINK} for latest updates.`,
-            node
-          )
+        if (node === escapeScope) {
+          escapeScope = undefined
         }
       }
-    },
-    leave(node: Node, parent?: Node) {
-      parent && parentStack.pop()
-      if (
-        (node.type === 'BlockStatement' && !isFunctionType(parent!)) ||
-        isFunctionType(node)
-      ) {
-        scopeStack.pop()
-        currentScope = scopeStack[scopeStack.length - 1] || null
-      }
-      if (node === escapeScope) {
-        escapeScope = undefined
-      }
+    })
+
+    return {
+      rootRefs: Object.keys(rootScope).filter(key => rootScope[key] === true),
+      importedHelpers: [...importedHelpers]
     }
-  })
+  }
 
   return {
-    rootRefs: Object.keys(rootScope).filter(key => rootScope[key] === true),
-    importedHelpers: [...importedHelpers]
+    transform,
+    transformAST,
+    shouldTransform
   }
 }
 
+const { transform, transformAST, shouldTransform } =
+  /*#__PURE__*/ createReactivityTransformer()
+export { transform, transformAST, shouldTransform }
+
 const RFC_LINK = `https://github.com/vuejs/rfcs/discussions/369`
 const hasWarned: Record<string, boolean> = {}
 
@@ -695,3 +738,7 @@ function warn(msg: string) {
     `\x1b[1m\x1b[33m[@vue/reactivity-transform]\x1b[0m\x1b[33m ${msg}\x1b[0m\n`
   )
 }
+
+function escapeRegExp(str: string) {
+  return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d')
+}