]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(complier-sfc): hoist literal constants for script (#5752)
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Tue, 28 Mar 2023 03:34:29 +0000 (11:34 +0800)
committerGitHub <noreply@github.com>
Tue, 28 Mar 2023 03:34:29 +0000 (11:34 +0800)
- Support using literal constants in macros
- fix useCssVars insert position edge case
- fix non-literal-const enum hoisting

close #5750

packages/compiler-core/src/options.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/__snapshots__/compileScriptHoistStatic.spec.ts.snap [new file with mode: 0644]
packages/compiler-sfc/__tests__/__snapshots__/cssVars.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/__tests__/compileScriptHoistStatic.spec.ts [new file with mode: 0644]
packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts
packages/compiler-sfc/src/compileScript.ts

index 1221772f70661a034ef585b3f0a0b73fa31914b7..65bbcb36dd600775695bc3e0468577901eceb39a 100644 (file)
@@ -112,7 +112,11 @@ export const enum BindingTypes {
   /**
    * declared by other options, e.g. computed, inject
    */
-  OPTIONS = 'options'
+  OPTIONS = 'options',
+  /**
+   * a literal constant, e.g. 'foo', 1, true
+   */
+  LITERAL_CONST = 'literal-const'
 }
 
 export type BindingMetadata = {
index 4bbe891e209860a1ffdda294610acf9df065dc16..52612edd612e6b702f86649fdf6c4aafeaec8cd3 100644 (file)
@@ -361,7 +361,8 @@ function resolveSetupReference(name: string, context: TransformContext) {
 
   const fromConst =
     checkType(BindingTypes.SETUP_CONST) ||
-    checkType(BindingTypes.SETUP_REACTIVE_CONST)
+    checkType(BindingTypes.SETUP_REACTIVE_CONST) ||
+    checkType(BindingTypes.LITERAL_CONST)
   if (fromConst) {
     return context.inline
       ? // in inline mode, const setup bindings (e.g. imports) can be used as-is
index 43c69559688b4f0a489c1f4cb62c76e48d9123de..112dc63cb7a8a2718b113a1acaeb9da34d02f130 100644 (file)
@@ -128,11 +128,7 @@ export function processExpression(
       const isDestructureAssignment =
         parent && isInDestructureAssignment(parent, parentStack)
 
-      if (
-        type === BindingTypes.SETUP_CONST ||
-        type === BindingTypes.SETUP_REACTIVE_CONST ||
-        localVars[raw]
-      ) {
+      if (isConst(type) || localVars[raw]) {
         return raw
       } else if (type === BindingTypes.SETUP_REF) {
         return `${raw}.value`
@@ -223,7 +219,7 @@ export function processExpression(
     if (!asParams && !isScopeVarReference && !isAllowedGlobal && !isLiteral) {
       // const bindings exposed from setup can be skipped for patching but
       // cannot be hoisted to module scope
-      if (bindingMetadata[node.content] === BindingTypes.SETUP_CONST) {
+      if (isConst(bindingMetadata[node.content])) {
         node.constType = ConstantTypes.CAN_SKIP_PATCH
       }
       node.content = rewriteIdentifier(rawExp)
@@ -372,3 +368,11 @@ export function stringifyExpression(exp: ExpressionNode | string): string {
       .join('')
   }
 }
+
+function isConst(type: unknown) {
+  return (
+    type === BindingTypes.SETUP_CONST ||
+    type === BindingTypes.LITERAL_CONST ||
+    type === BindingTypes.SETUP_REACTIVE_CONST
+  )
+}
index 2813a0be78800c9b29abb9acfd5d98f47b4b7354..4615b20fe738d86345a432e34a6fef7643ba8a3f 100644 (file)
@@ -1,11 +1,12 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 exports[`SFC analyze <script> bindings > auto name inference > basic 1`] = `
-"export default {
+"const a = 1
+export default {
   __name: 'FooBar',
   setup(__props, { expose }) {
   expose();
-const a = 1
+
 return { a }
 }
 
@@ -683,7 +684,9 @@ return { props, get x() { return x } }
 `;
 
 exports[`SFC compile <script setup> > defineProps() 1`] = `
-"export default {
+"const bar = 1
+
+export default {
   props: {
   foo: String
 },
@@ -693,7 +696,6 @@ exports[`SFC compile <script setup> > defineProps() 1`] = `
 const props = __props;
 
 
-const bar = 1
 
 return { props, bar }
 }
@@ -755,12 +757,12 @@ return { a, props, emit }
 exports[`SFC compile <script setup> > dev mode import usage check > TS annotations 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
 import { Foo, Bar, Baz, Qux, Fred } from './x'
+        const a = 1
         
 export default /*#__PURE__*/_defineComponent({
   setup(__props, { expose }) {
   expose();
 
-        const a = 1
         function b() {}
         
 return { a, b, get Baz() { return Baz } }
@@ -772,12 +774,12 @@ return { a, b, get Baz() { return Baz } }
 exports[`SFC compile <script setup> > dev mode import usage check > attribute expressions 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
 import { bar, baz } from './x'
+        const cond = true
         
 export default /*#__PURE__*/_defineComponent({
   setup(__props, { expose }) {
   expose();
 
-        const cond = true
         
 return { cond, get bar() { return bar }, get baz() { return baz } }
 }
@@ -788,12 +790,12 @@ return { cond, get bar() { return bar }, get baz() { return baz } }
 exports[`SFC compile <script setup> > dev mode import usage check > components 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
 import { FooBar, FooBaz, FooQux, foo } from './x'
+        const fooBar: FooBar = 1
         
 export default /*#__PURE__*/_defineComponent({
   setup(__props, { expose }) {
   expose();
 
-        const fooBar: FooBar = 1
         
 return { fooBar, get FooBaz() { return FooBaz }, get FooQux() { return FooQux }, get foo() { return foo } }
 }
@@ -886,7 +888,9 @@ return { get bar() { return bar } }
 `;
 
 exports[`SFC compile <script setup> > errors > should allow defineProps/Emit() referencing scope var 1`] = `
-"export default {
+"const bar = 1
+          
+export default {
   props: {
             foo: {
               default: bar => bar + 1
@@ -898,7 +902,6 @@ exports[`SFC compile <script setup> > errors > should allow defineProps/Emit() r
   setup(__props, { expose }) {
   expose();
 
-          const bar = 1
           
           
         
@@ -1722,8 +1725,7 @@ return { Foo }
 
 exports[`SFC compile <script setup> > with TypeScript > runtime Enum in normal script 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
-enum Foo { A = 123 }
-        
+
           export enum D { D = \\"D\\" }
           const enum C { C = \\"C\\" }
           enum B { B = \\"B\\" }
@@ -1732,6 +1734,7 @@ export default /*#__PURE__*/_defineComponent({
   setup(__props, { expose }) {
   expose();
 
+        enum Foo { A = 123 }
         
 return { D, C, B, Foo }
 }
diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScriptHoistStatic.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScriptHoistStatic.spec.ts.snap
new file mode 100644 (file)
index 0000000..5b7aa43
--- /dev/null
@@ -0,0 +1,132 @@
+// Vitest Snapshot v1
+
+exports[`sfc hoist static > should enable when only script setup 1`] = `
+"const foo = 'bar'
+    
+export default {
+  setup(__props) {
+
+    const foo = 'bar'
+    
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc hoist static > should hoist expressions 1`] = `
+"const unary = !false
+    const binary = 1 + 2
+    const conditional = 1 ? 2 : 3
+    const sequence = (1, true, 'foo', 1)
+    
+export default {
+  setup(__props) {
+
+    
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc hoist static > should hoist literal value 1`] = `
+"const string = 'default value'
+    const number = 123
+    const boolean = false
+    const nil = null
+    const bigint = 100n
+    const template = \`str\`
+    const regex = /.*/g
+    
+export default {
+  setup(__props) {
+
+    
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc hoist static > should hoist w/ defineProps/Emits 1`] = `
+"const defaultValue = 'default value'
+    
+export default {
+  props: {
+      foo: {
+        default: defaultValue
+      }
+    },
+  setup(__props) {
+
+    
+    
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc hoist static > should not hoist a constant initialized to a reference value 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+  setup(__props) {
+
+    const KEY1 = Boolean
+    const KEY2 = [Boolean]
+    const KEY3 = [getCurrentInstance()]
+    let i = 0;
+    const KEY4 = (i++, 'foo')
+    enum KEY5 {
+      FOO = 1,
+      BAR = getCurrentInstance(),
+    }
+    const KEY6 = \`template\${i}\`
+    
+return () => {}
+}
+
+})"
+`;
+
+exports[`sfc hoist static > should not hoist a function or class 1`] = `
+"export default {
+  setup(__props) {
+
+    const fn = () => {}
+    function fn2() {}
+    class Foo {}
+    
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc hoist static > should not hoist a object or array 1`] = `
+"export default {
+  setup(__props) {
+
+    const obj = { foo: 'bar' }
+    const arr = [1, 2, 3]
+    
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc hoist static > should not hoist a variable 1`] = `
+"export default {
+  setup(__props) {
+
+    let KEY1 = 'default value'
+    var KEY2 = 123
+    
+return () => {}
+}
+
+}"
+`;
index 72de2d0e4763a8474a6a30ca08d888ffd39e86e5..aba892f93bc28a433470f64956b60610dc238960 100644 (file)
@@ -51,7 +51,7 @@ export default __default__"
 
 exports[`CSS vars injection > codegen > should ignore comments 1`] = `
 "import { useCssVars as _useCssVars, unref as _unref } from 'vue'
-
+const color = 'red';const width = 100
 export default {
   setup(__props, { expose }) {
   expose();
@@ -59,7 +59,7 @@ export default {
 _useCssVars(_ctx => ({
   \\"xxxxxxxx-width\\": (width)
 }))
-const color = 'red';const width = 100
+
 return { color, width }
 }
 
@@ -92,7 +92,7 @@ return { get a() { return a }, set a(v) { a = v }, get b() { return b }, set b(v
 
 exports[`CSS vars injection > codegen > w/ <script setup> 1`] = `
 "import { useCssVars as _useCssVars, unref as _unref } from 'vue'
-
+const color = 'red'
 export default {
   setup(__props, { expose }) {
   expose();
@@ -100,7 +100,7 @@ export default {
 _useCssVars(_ctx => ({
   \\"xxxxxxxx-color\\": (color)
 }))
-const color = 'red'
+
 return { color }
 }
 
@@ -109,7 +109,8 @@ return { color }
 
 exports[`CSS vars injection > codegen > w/ <script setup> using the same var multiple times 1`] = `
 "import { useCssVars as _useCssVars, unref as _unref } from 'vue'
-
+const color = 'red'
+        
 export default {
   setup(__props, { expose }) {
   expose();
@@ -118,7 +119,6 @@ _useCssVars(_ctx => ({
   \\"xxxxxxxx-color\\": (color)
 }))
 
-        const color = 'red'
         
 return { color }
 }
@@ -146,6 +146,7 @@ export default __default__"
 exports[`CSS vars injection > w/ <script setup> binding analysis 1`] = `
 "import { useCssVars as _useCssVars, unref as _unref } from 'vue'
 import { ref } from 'vue'
+        const color = 'red'
         
 export default {
   props: {
@@ -160,7 +161,6 @@ _useCssVars(_ctx => ({
   \\"xxxxxxxx-foo\\": (__props.foo)
 }))
 
-        const color = 'red'
         const size = ref('10px')
         
         
index 94d622037e1e672513d108cc445ed742246548d6..27c3e26c423934f16b3928656ff2de2bc96f2b5f 100644 (file)
@@ -28,12 +28,12 @@ describe('SFC compile <script setup>', () => {
     expect(bindings).toStrictEqual({
       x: BindingTypes.SETUP_MAYBE_REF,
       a: BindingTypes.SETUP_LET,
-      b: BindingTypes.SETUP_CONST,
+      b: BindingTypes.LITERAL_CONST,
       c: BindingTypes.SETUP_CONST,
       d: BindingTypes.SETUP_CONST,
       xx: BindingTypes.SETUP_MAYBE_REF,
       aa: BindingTypes.SETUP_LET,
-      bb: BindingTypes.SETUP_CONST,
+      bb: BindingTypes.LITERAL_CONST,
       cc: BindingTypes.SETUP_CONST,
       dd: BindingTypes.SETUP_CONST
     })
@@ -71,7 +71,7 @@ const bar = 1
     // should analyze bindings
     expect(bindings).toStrictEqual({
       foo: BindingTypes.PROPS,
-      bar: BindingTypes.SETUP_CONST,
+      bar: BindingTypes.LITERAL_CONST,
       props: BindingTypes.SETUP_REACTIVE_CONST
     })
 
@@ -1422,7 +1422,7 @@ const emit = defineEmits(['a', 'b'])
       )
       assertCode(content)
       expect(bindings).toStrictEqual({
-        Foo: BindingTypes.SETUP_CONST
+        Foo: BindingTypes.LITERAL_CONST
       })
     })
 
@@ -1439,10 +1439,10 @@ const emit = defineEmits(['a', 'b'])
       )
       assertCode(content)
       expect(bindings).toStrictEqual({
-        D: BindingTypes.SETUP_CONST,
-        C: BindingTypes.SETUP_CONST,
-        B: BindingTypes.SETUP_CONST,
-        Foo: BindingTypes.SETUP_CONST
+        D: BindingTypes.LITERAL_CONST,
+        C: BindingTypes.LITERAL_CONST,
+        B: BindingTypes.LITERAL_CONST,
+        Foo: BindingTypes.LITERAL_CONST
       })
     })
 
@@ -1450,11 +1450,12 @@ const emit = defineEmits(['a', 'b'])
       const { content, bindings } = compile(
         `<script setup lang="ts">
         const enum Foo { A = 123 }
-        </script>`
+        </script>`,
+        { hoistStatic: true }
       )
       assertCode(content)
       expect(bindings).toStrictEqual({
-        Foo: BindingTypes.SETUP_CONST
+        Foo: BindingTypes.LITERAL_CONST
       })
     })
 
@@ -1633,7 +1634,7 @@ const emit = defineEmits(['a', 'b'])
     test('defineProps/Emit() referencing local var', () => {
       expect(() =>
         compile(`<script setup>
-        const bar = 1
+        let bar = 1
         defineProps({
           foo: {
             default: () => bar
@@ -1644,7 +1645,7 @@ const emit = defineEmits(['a', 'b'])
 
       expect(() =>
         compile(`<script setup>
-        const bar = 'hello'
+        let bar = 'hello'
         defineEmits([bar])
         </script>`)
       ).toThrow(`cannot reference locally declared variables`)
@@ -1785,7 +1786,7 @@ describe('SFC analyze <script> bindings', () => {
       </script>
     `)
     expect(bindings).toStrictEqual({
-      foo: BindingTypes.SETUP_CONST
+      foo: BindingTypes.LITERAL_CONST
     })
   })
 
@@ -1951,7 +1952,7 @@ describe('SFC analyze <script> bindings', () => {
       r: BindingTypes.SETUP_CONST,
       a: BindingTypes.SETUP_REF,
       b: BindingTypes.SETUP_LET,
-      c: BindingTypes.SETUP_CONST,
+      c: BindingTypes.LITERAL_CONST,
       d: BindingTypes.SETUP_MAYBE_REF,
       e: BindingTypes.SETUP_LET,
       foo: BindingTypes.PROPS
diff --git a/packages/compiler-sfc/__tests__/compileScriptHoistStatic.spec.ts b/packages/compiler-sfc/__tests__/compileScriptHoistStatic.spec.ts
new file mode 100644 (file)
index 0000000..032a8de
--- /dev/null
@@ -0,0 +1,190 @@
+import { BindingTypes } from '@vue/compiler-core'
+import { SFCScriptCompileOptions } from '../src'
+import { compileSFCScript, assertCode } from './utils'
+
+describe('sfc hoist static', () => {
+  function compile(src: string, options?: Partial<SFCScriptCompileOptions>) {
+    return compileSFCScript(src, {
+      inlineTemplate: true,
+      hoistStatic: true,
+      ...options
+    })
+  }
+
+  test('should hoist literal value', () => {
+    const code = `
+    const string = 'default value'
+    const number = 123
+    const boolean = false
+    const nil = null
+    const bigint = 100n
+    const template = \`str\`
+    const regex = /.*/g
+    `.trim()
+    const { content, bindings } = compile(`
+    <script setup>
+    ${code}
+    </script>
+    `)
+
+    // should hoist to first line
+    expect(content.startsWith(code)).toBe(true)
+    expect(bindings).toStrictEqual({
+      string: BindingTypes.LITERAL_CONST,
+      number: BindingTypes.LITERAL_CONST,
+      boolean: BindingTypes.LITERAL_CONST,
+      nil: BindingTypes.LITERAL_CONST,
+      bigint: BindingTypes.LITERAL_CONST,
+      template: BindingTypes.LITERAL_CONST,
+      regex: BindingTypes.LITERAL_CONST
+    })
+    assertCode(content)
+  })
+
+  test('should hoist expressions', () => {
+    const code = `
+    const unary = !false
+    const binary = 1 + 2
+    const conditional = 1 ? 2 : 3
+    const sequence = (1, true, 'foo', 1)
+    `.trim()
+    const { content, bindings } = compile(`
+    <script setup>
+    ${code}
+    </script>
+    `)
+    // should hoist to first line
+    expect(content.startsWith(code)).toBe(true)
+    expect(bindings).toStrictEqual({
+      binary: BindingTypes.LITERAL_CONST,
+      conditional: BindingTypes.LITERAL_CONST,
+      unary: BindingTypes.LITERAL_CONST,
+      sequence: BindingTypes.LITERAL_CONST
+    })
+    assertCode(content)
+  })
+
+  test('should hoist w/ defineProps/Emits', () => {
+    const hoistCode = `const defaultValue = 'default value'`
+    const { content, bindings } = compile(`
+    <script setup>
+    ${hoistCode}
+    defineProps({
+      foo: {
+        default: defaultValue
+      }
+    })
+    </script>
+    `)
+
+    // should hoist to first line
+    expect(content.startsWith(hoistCode)).toBe(true)
+    expect(bindings).toStrictEqual({
+      foo: BindingTypes.PROPS,
+      defaultValue: BindingTypes.LITERAL_CONST
+    })
+    assertCode(content)
+  })
+
+  test('should not hoist a variable', () => {
+    const code = `
+    let KEY1 = 'default value'
+    var KEY2 = 123
+    `.trim()
+    const { content, bindings } = compile(`
+    <script setup>
+    ${code}
+    </script>
+    `)
+    expect(bindings).toStrictEqual({
+      KEY1: BindingTypes.SETUP_LET,
+      KEY2: BindingTypes.SETUP_LET
+    })
+    expect(content).toMatch(`setup(__props) {\n\n    ${code}`)
+    assertCode(content)
+  })
+
+  test('should not hoist a constant initialized to a reference value', () => {
+    const code = `
+    const KEY1 = Boolean
+    const KEY2 = [Boolean]
+    const KEY3 = [getCurrentInstance()]
+    let i = 0;
+    const KEY4 = (i++, 'foo')
+    enum KEY5 {
+      FOO = 1,
+      BAR = getCurrentInstance(),
+    }
+    const KEY6 = \`template\${i}\`
+    `.trim()
+    const { content, bindings } = compile(`
+    <script setup lang="ts">
+    ${code}
+    </script>
+    `)
+    expect(bindings).toStrictEqual({
+      KEY1: BindingTypes.SETUP_MAYBE_REF,
+      KEY2: BindingTypes.SETUP_CONST,
+      KEY3: BindingTypes.SETUP_CONST,
+      KEY4: BindingTypes.SETUP_CONST,
+      KEY5: BindingTypes.SETUP_CONST,
+      KEY6: BindingTypes.SETUP_CONST,
+      i: BindingTypes.SETUP_LET
+    })
+    expect(content).toMatch(`setup(__props) {\n\n    ${code}`)
+    assertCode(content)
+  })
+
+  test('should not hoist a object or array', () => {
+    const code = `
+    const obj = { foo: 'bar' }
+    const arr = [1, 2, 3]
+    `.trim()
+    const { content, bindings } = compile(`
+    <script setup>
+    ${code}
+    </script>
+    `)
+    expect(bindings).toStrictEqual({
+      arr: BindingTypes.SETUP_CONST,
+      obj: BindingTypes.SETUP_CONST
+    })
+    expect(content).toMatch(`setup(__props) {\n\n    ${code}`)
+    assertCode(content)
+  })
+
+  test('should not hoist a function or class', () => {
+    const code = `
+    const fn = () => {}
+    function fn2() {}
+    class Foo {}
+    `.trim()
+    const { content, bindings } = compile(`
+    <script setup>
+    ${code}
+    </script>
+    `)
+    expect(bindings).toStrictEqual({
+      Foo: BindingTypes.SETUP_CONST,
+      fn: BindingTypes.SETUP_CONST,
+      fn2: BindingTypes.SETUP_CONST
+    })
+    expect(content).toMatch(`setup(__props) {\n\n    ${code}`)
+    assertCode(content)
+  })
+
+  test('should enable when only script setup', () => {
+    const { content, bindings } = compile(`
+    <script>
+    const foo = 'bar'
+    </script>
+    <script setup>
+    const foo = 'bar'
+    </script>
+    `)
+    expect(bindings).toStrictEqual({
+      foo: BindingTypes.LITERAL_CONST
+    })
+    assertCode(content)
+  })
+})
index 05c7989b8f1da6818eb59eb7f17d27324d5bb837..5401a9e030512dfd1cf9813c7116cf22648c2879 100644 (file)
@@ -43,8 +43,8 @@ describe('sfc props transform', () => {
     assertCode(content)
     expect(bindings).toStrictEqual({
       foo: BindingTypes.PROPS,
-      bar: BindingTypes.SETUP_CONST,
-      hello: BindingTypes.SETUP_CONST
+      bar: BindingTypes.LITERAL_CONST,
+      hello: BindingTypes.LITERAL_CONST
     })
   })
 
@@ -259,7 +259,7 @@ describe('sfc props transform', () => {
       expect(() =>
         compile(
           `<script setup>
-          const x = 1
+          let x = 1
           const {
             foo = () => x
           } = defineProps(['foo'])
index e05b09a17f8f5e11fdf93f9429eb8d78dd55d8b9..8f2a5adeefd9cd5df3aebd2cb46011e494b96007 100644 (file)
@@ -111,6 +111,13 @@ export interface SFCScriptCompileOptions {
    * options passed to `compiler-dom`.
    */
   templateOptions?: Partial<SFCTemplateCompileOptions>
+
+  /**
+   * Hoist <script setup> static constants.
+   * - Only enables when one `<script setup>` exists.
+   * @default true
+   */
+  hoistStatic?: boolean
 }
 
 export interface ImportBinding {
@@ -144,6 +151,7 @@ export function compileScript(
   const enablePropsTransform = !!options.reactivityTransform
   const isProd = !!options.isProd
   const genSourceMap = options.sourceMap !== false
+  const hoistStatic = options.hoistStatic !== false && !script
   let refBindings: string[] | undefined
 
   if (!options.id) {
@@ -743,7 +751,8 @@ export function compileScript(
   function checkInvalidScopeReference(node: Node | undefined, method: string) {
     if (!node) return
     walkIdentifiers(node, id => {
-      if (setupBindings[id.name]) {
+      const binding = setupBindings[id.name]
+      if (binding && (binding !== BindingTypes.LITERAL_CONST || !hoistStatic)) {
         error(
           `\`${method}()\` in <script setup> cannot reference locally ` +
             `declared variables because it will be hoisted outside of the ` +
@@ -905,7 +914,7 @@ export function compileScript(
         destructured.default.start!,
         destructured.default.end!
       )
-      const isLiteral = destructured.default.type.endsWith('Literal')
+      const isLiteral = isLiteralNode(destructured.default)
       return isLiteral ? value : `() => (${value})`
     }
   }
@@ -1276,14 +1285,21 @@ export function compileScript(
       }
     }
 
+    let isAllLiteral = false
     // walk declarations to record declared bindings
     if (
       (node.type === 'VariableDeclaration' ||
         node.type === 'FunctionDeclaration' ||
-        node.type === 'ClassDeclaration') &&
+        node.type === 'ClassDeclaration' ||
+        node.type === 'TSEnumDeclaration') &&
       !node.declare
     ) {
-      walkDeclaration(node, setupBindings, vueImportAliases)
+      isAllLiteral = walkDeclaration(node, setupBindings, vueImportAliases)
+    }
+
+    // hoist literal constants
+    if (hoistStatic && isAllLiteral) {
+      hoistNode(node)
     }
 
     // walk statements & named exports / variable declarations for top level
@@ -1342,17 +1358,13 @@ export function compileScript(
     }
 
     if (isTS) {
-      // runtime enum
-      if (node.type === 'TSEnumDeclaration') {
-        registerBinding(setupBindings, node.id, BindingTypes.SETUP_CONST)
-      }
-
       // move all Type declarations to outer scope
       if (
-        node.type.startsWith('TS') ||
-        (node.type === 'ExportNamedDeclaration' &&
-          node.exportKind === 'type') ||
-        (node.type === 'VariableDeclaration' && node.declare)
+        (node.type.startsWith('TS') ||
+          (node.type === 'ExportNamedDeclaration' &&
+            node.exportKind === 'type') ||
+          (node.type === 'VariableDeclaration' && node.declare)) &&
+        node.type !== 'TSEnumDeclaration'
       ) {
         recordType(node, declaredTypes)
         hoistNode(node)
@@ -1474,7 +1486,7 @@ export function compileScript(
   ) {
     helperImports.add(CSS_VARS_HELPER)
     helperImports.add('unref')
-    s.prependRight(
+    s.prependLeft(
       startOffset,
       `\n${genCssVarsCode(cssVars, bindingMetadata, scopeId, isProd)}\n`
     )
@@ -1774,9 +1786,17 @@ function walkDeclaration(
   node: Declaration,
   bindings: Record<string, BindingTypes>,
   userImportAliases: Record<string, string>
-) {
+): boolean {
+  let isAllLiteral = false
+
   if (node.type === 'VariableDeclaration') {
     const isConst = node.kind === 'const'
+    isAllLiteral =
+      isConst &&
+      node.declarations.every(
+        decl => decl.id.type === 'Identifier' && isStaticNode(decl.init!)
+      )
+
     // export const foo = ...
     for (const { id, init } of node.declarations) {
       const isDefineCall = !!(
@@ -1789,7 +1809,9 @@ function walkDeclaration(
       if (id.type === 'Identifier') {
         let bindingType
         const userReactiveBinding = userImportAliases['reactive']
-        if (isCallOf(init, userReactiveBinding)) {
+        if (isAllLiteral || (isConst && isStaticNode(init!))) {
+          bindingType = BindingTypes.LITERAL_CONST
+        } else if (isCallOf(init, userReactiveBinding)) {
           // treat reactive() calls as let since it's meant to be mutable
           bindingType = isConst
             ? BindingTypes.SETUP_REACTIVE_CONST
@@ -1824,8 +1846,14 @@ function walkDeclaration(
         }
       }
     }
+  } else if (node.type === 'TSEnumDeclaration') {
+    isAllLiteral = node.members.every(
+      member => !member.initializer || isStaticNode(member.initializer)
+    )
+    bindings[node.id!.name] = isAllLiteral
+      ? BindingTypes.LITERAL_CONST
+      : BindingTypes.SETUP_CONST
   } else if (
-    node.type === 'TSEnumDeclaration' ||
     node.type === 'FunctionDeclaration' ||
     node.type === 'ClassDeclaration'
   ) {
@@ -1833,6 +1861,8 @@ function walkDeclaration(
     // export declarations must be named.
     bindings[node.id!.name] = BindingTypes.SETUP_CONST
   }
+
+  return isAllLiteral
 }
 
 function walkObjectPattern(
@@ -2138,13 +2168,53 @@ function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
         userReactiveImport
       )
     default:
-      if (node.type.endsWith('Literal')) {
+      if (isLiteralNode(node)) {
         return true
       }
       return false
   }
 }
 
+function isStaticNode(node: Node): boolean {
+  switch (node.type) {
+    case 'UnaryExpression': // void 0, !true
+      return isStaticNode(node.argument)
+
+    case 'LogicalExpression': // 1 > 2
+    case 'BinaryExpression': // 1 + 2
+      return isStaticNode(node.left) && isStaticNode(node.right)
+
+    case 'ConditionalExpression': {
+      // 1 ? 2 : 3
+      return (
+        isStaticNode(node.test) &&
+        isStaticNode(node.consequent) &&
+        isStaticNode(node.alternate)
+      )
+    }
+
+    case 'SequenceExpression': // (1, 2)
+    case 'TemplateLiteral': // `foo${1}`
+      return node.expressions.every(expr => isStaticNode(expr))
+
+    case 'ParenthesizedExpression': // (1)
+    case 'TSNonNullExpression': // 1!
+    case 'TSAsExpression': // 1 as number
+    case 'TSTypeAssertion': // (<number>2)
+      return isStaticNode(node.expression)
+
+    default:
+      if (isLiteralNode(node)) {
+        return true
+      }
+      return false
+  }
+}
+
+function isLiteralNode(node: Node) {
+  return node.type.endsWith('Literal')
+}
+
 /**
  * Analyze bindings in normal `<script>`
  * Note that `compileScriptSetup` already analyzes bindings as part of its