feat(complier-sfc): hoist literal constants for script (#5752)

- Support using literal constants in macros
- fix useCssVars insert position edge case
- fix non-literal-const enum hoisting

close #5750
This commit is contained in:
三咲智子 Kevin Deng 2023-03-28 11:34:29 +08:00 committed by GitHub
parent e224922e97
commit 7def8b15b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 466 additions and 61 deletions

View File

@ -112,7 +112,11 @@ export const enum BindingTypes {
/** /**
* declared by other options, e.g. computed, inject * 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 = { export type BindingMetadata = {

View File

@ -361,7 +361,8 @@ function resolveSetupReference(name: string, context: TransformContext) {
const fromConst = const fromConst =
checkType(BindingTypes.SETUP_CONST) || checkType(BindingTypes.SETUP_CONST) ||
checkType(BindingTypes.SETUP_REACTIVE_CONST) checkType(BindingTypes.SETUP_REACTIVE_CONST) ||
checkType(BindingTypes.LITERAL_CONST)
if (fromConst) { if (fromConst) {
return context.inline return context.inline
? // in inline mode, const setup bindings (e.g. imports) can be used as-is ? // in inline mode, const setup bindings (e.g. imports) can be used as-is

View File

@ -128,11 +128,7 @@ export function processExpression(
const isDestructureAssignment = const isDestructureAssignment =
parent && isInDestructureAssignment(parent, parentStack) parent && isInDestructureAssignment(parent, parentStack)
if ( if (isConst(type) || localVars[raw]) {
type === BindingTypes.SETUP_CONST ||
type === BindingTypes.SETUP_REACTIVE_CONST ||
localVars[raw]
) {
return raw return raw
} else if (type === BindingTypes.SETUP_REF) { } else if (type === BindingTypes.SETUP_REF) {
return `${raw}.value` return `${raw}.value`
@ -223,7 +219,7 @@ export function processExpression(
if (!asParams && !isScopeVarReference && !isAllowedGlobal && !isLiteral) { if (!asParams && !isScopeVarReference && !isAllowedGlobal && !isLiteral) {
// const bindings exposed from setup can be skipped for patching but // const bindings exposed from setup can be skipped for patching but
// cannot be hoisted to module scope // 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.constType = ConstantTypes.CAN_SKIP_PATCH
} }
node.content = rewriteIdentifier(rawExp) node.content = rewriteIdentifier(rawExp)
@ -372,3 +368,11 @@ export function stringifyExpression(exp: ExpressionNode | string): string {
.join('') .join('')
} }
} }
function isConst(type: unknown) {
return (
type === BindingTypes.SETUP_CONST ||
type === BindingTypes.LITERAL_CONST ||
type === BindingTypes.SETUP_REACTIVE_CONST
)
}

View File

@ -1,11 +1,12 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`SFC analyze <script> bindings > auto name inference > basic 1`] = ` exports[`SFC analyze <script> bindings > auto name inference > basic 1`] = `
"export default { "const a = 1
export default {
__name: 'FooBar', __name: 'FooBar',
setup(__props, { expose }) { setup(__props, { expose }) {
expose(); expose();
const a = 1
return { a } return { a }
} }
@ -683,7 +684,9 @@ return { props, get x() { return x } }
`; `;
exports[`SFC compile <script setup> > defineProps() 1`] = ` exports[`SFC compile <script setup> > defineProps() 1`] = `
"export default { "const bar = 1
export default {
props: { props: {
foo: String foo: String
}, },
@ -693,7 +696,6 @@ exports[`SFC compile <script setup> > defineProps() 1`] = `
const props = __props; const props = __props;
const bar = 1
return { props, bar } return { props, bar }
} }
@ -755,12 +757,12 @@ return { a, props, emit }
exports[`SFC compile <script setup> > dev mode import usage check > TS annotations 1`] = ` exports[`SFC compile <script setup> > dev mode import usage check > TS annotations 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { Foo, Bar, Baz, Qux, Fred } from './x' import { Foo, Bar, Baz, Qux, Fred } from './x'
const a = 1
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
setup(__props, { expose }) { setup(__props, { expose }) {
expose(); expose();
const a = 1
function b() {} function b() {}
return { a, b, get Baz() { return Baz } } 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`] = ` exports[`SFC compile <script setup> > dev mode import usage check > attribute expressions 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { bar, baz } from './x' import { bar, baz } from './x'
const cond = true
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
setup(__props, { expose }) { setup(__props, { expose }) {
expose(); expose();
const cond = true
return { cond, get bar() { return bar }, get baz() { return baz } } 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`] = ` exports[`SFC compile <script setup> > dev mode import usage check > components 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
import { FooBar, FooBaz, FooQux, foo } from './x' import { FooBar, FooBaz, FooQux, foo } from './x'
const fooBar: FooBar = 1
export default /*#__PURE__*/_defineComponent({ export default /*#__PURE__*/_defineComponent({
setup(__props, { expose }) { setup(__props, { expose }) {
expose(); expose();
const fooBar: FooBar = 1
return { fooBar, get FooBaz() { return FooBaz }, get FooQux() { return FooQux }, get foo() { return foo } } 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`] = ` exports[`SFC compile <script setup> > errors > should allow defineProps/Emit() referencing scope var 1`] = `
"export default { "const bar = 1
export default {
props: { props: {
foo: { foo: {
default: bar => bar + 1 default: bar => bar + 1
@ -898,7 +902,6 @@ exports[`SFC compile <script setup> > errors > should allow defineProps/Emit() r
setup(__props, { expose }) { setup(__props, { expose }) {
expose(); expose();
const bar = 1
@ -1722,7 +1725,6 @@ return { Foo }
exports[`SFC compile <script setup> > with TypeScript > runtime Enum in normal script 1`] = ` exports[`SFC compile <script setup> > with TypeScript > runtime Enum in normal script 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
enum Foo { A = 123 }
export enum D { D = \\"D\\" } export enum D { D = \\"D\\" }
const enum C { C = \\"C\\" } const enum C { C = \\"C\\" }
@ -1732,6 +1734,7 @@ export default /*#__PURE__*/_defineComponent({
setup(__props, { expose }) { setup(__props, { expose }) {
expose(); expose();
enum Foo { A = 123 }
return { D, C, B, Foo } return { D, C, B, Foo }
} }

View File

@ -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 () => {}
}
}"
`;

View File

@ -51,7 +51,7 @@ export default __default__"
exports[`CSS vars injection > codegen > should ignore comments 1`] = ` exports[`CSS vars injection > codegen > should ignore comments 1`] = `
"import { useCssVars as _useCssVars, unref as _unref } from 'vue' "import { useCssVars as _useCssVars, unref as _unref } from 'vue'
const color = 'red';const width = 100
export default { export default {
setup(__props, { expose }) { setup(__props, { expose }) {
expose(); expose();
@ -59,7 +59,7 @@ export default {
_useCssVars(_ctx => ({ _useCssVars(_ctx => ({
\\"xxxxxxxx-width\\": (width) \\"xxxxxxxx-width\\": (width)
})) }))
const color = 'red';const width = 100
return { color, width } 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`] = ` exports[`CSS vars injection > codegen > w/ <script setup> 1`] = `
"import { useCssVars as _useCssVars, unref as _unref } from 'vue' "import { useCssVars as _useCssVars, unref as _unref } from 'vue'
const color = 'red'
export default { export default {
setup(__props, { expose }) { setup(__props, { expose }) {
expose(); expose();
@ -100,7 +100,7 @@ export default {
_useCssVars(_ctx => ({ _useCssVars(_ctx => ({
\\"xxxxxxxx-color\\": (color) \\"xxxxxxxx-color\\": (color)
})) }))
const color = 'red'
return { color } return { color }
} }
@ -109,6 +109,7 @@ return { color }
exports[`CSS vars injection > codegen > w/ <script setup> using the same var multiple times 1`] = ` exports[`CSS vars injection > codegen > w/ <script setup> using the same var multiple times 1`] = `
"import { useCssVars as _useCssVars, unref as _unref } from 'vue' "import { useCssVars as _useCssVars, unref as _unref } from 'vue'
const color = 'red'
export default { export default {
setup(__props, { expose }) { setup(__props, { expose }) {
@ -118,7 +119,6 @@ _useCssVars(_ctx => ({
\\"xxxxxxxx-color\\": (color) \\"xxxxxxxx-color\\": (color)
})) }))
const color = 'red'
return { color } return { color }
} }
@ -146,6 +146,7 @@ export default __default__"
exports[`CSS vars injection > w/ <script setup> binding analysis 1`] = ` exports[`CSS vars injection > w/ <script setup> binding analysis 1`] = `
"import { useCssVars as _useCssVars, unref as _unref } from 'vue' "import { useCssVars as _useCssVars, unref as _unref } from 'vue'
import { ref } from 'vue' import { ref } from 'vue'
const color = 'red'
export default { export default {
props: { props: {
@ -160,7 +161,6 @@ _useCssVars(_ctx => ({
\\"xxxxxxxx-foo\\": (__props.foo) \\"xxxxxxxx-foo\\": (__props.foo)
})) }))
const color = 'red'
const size = ref('10px') const size = ref('10px')

View File

@ -28,12 +28,12 @@ describe('SFC compile <script setup>', () => {
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
x: BindingTypes.SETUP_MAYBE_REF, x: BindingTypes.SETUP_MAYBE_REF,
a: BindingTypes.SETUP_LET, a: BindingTypes.SETUP_LET,
b: BindingTypes.SETUP_CONST, b: BindingTypes.LITERAL_CONST,
c: BindingTypes.SETUP_CONST, c: BindingTypes.SETUP_CONST,
d: BindingTypes.SETUP_CONST, d: BindingTypes.SETUP_CONST,
xx: BindingTypes.SETUP_MAYBE_REF, xx: BindingTypes.SETUP_MAYBE_REF,
aa: BindingTypes.SETUP_LET, aa: BindingTypes.SETUP_LET,
bb: BindingTypes.SETUP_CONST, bb: BindingTypes.LITERAL_CONST,
cc: BindingTypes.SETUP_CONST, cc: BindingTypes.SETUP_CONST,
dd: BindingTypes.SETUP_CONST dd: BindingTypes.SETUP_CONST
}) })
@ -71,7 +71,7 @@ const bar = 1
// should analyze bindings // should analyze bindings
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS, foo: BindingTypes.PROPS,
bar: BindingTypes.SETUP_CONST, bar: BindingTypes.LITERAL_CONST,
props: BindingTypes.SETUP_REACTIVE_CONST props: BindingTypes.SETUP_REACTIVE_CONST
}) })
@ -1422,7 +1422,7 @@ const emit = defineEmits(['a', 'b'])
) )
assertCode(content) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
Foo: BindingTypes.SETUP_CONST Foo: BindingTypes.LITERAL_CONST
}) })
}) })
@ -1439,10 +1439,10 @@ const emit = defineEmits(['a', 'b'])
) )
assertCode(content) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
D: BindingTypes.SETUP_CONST, D: BindingTypes.LITERAL_CONST,
C: BindingTypes.SETUP_CONST, C: BindingTypes.LITERAL_CONST,
B: BindingTypes.SETUP_CONST, B: BindingTypes.LITERAL_CONST,
Foo: BindingTypes.SETUP_CONST Foo: BindingTypes.LITERAL_CONST
}) })
}) })
@ -1450,11 +1450,12 @@ const emit = defineEmits(['a', 'b'])
const { content, bindings } = compile( const { content, bindings } = compile(
`<script setup lang="ts"> `<script setup lang="ts">
const enum Foo { A = 123 } const enum Foo { A = 123 }
</script>` </script>`,
{ hoistStatic: true }
) )
assertCode(content) assertCode(content)
expect(bindings).toStrictEqual({ 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', () => { test('defineProps/Emit() referencing local var', () => {
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
const bar = 1 let bar = 1
defineProps({ defineProps({
foo: { foo: {
default: () => bar default: () => bar
@ -1644,7 +1645,7 @@ const emit = defineEmits(['a', 'b'])
expect(() => expect(() =>
compile(`<script setup> compile(`<script setup>
const bar = 'hello' let bar = 'hello'
defineEmits([bar]) defineEmits([bar])
</script>`) </script>`)
).toThrow(`cannot reference locally declared variables`) ).toThrow(`cannot reference locally declared variables`)
@ -1785,7 +1786,7 @@ describe('SFC analyze <script> bindings', () => {
</script> </script>
`) `)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_CONST foo: BindingTypes.LITERAL_CONST
}) })
}) })
@ -1951,7 +1952,7 @@ describe('SFC analyze <script> bindings', () => {
r: BindingTypes.SETUP_CONST, r: BindingTypes.SETUP_CONST,
a: BindingTypes.SETUP_REF, a: BindingTypes.SETUP_REF,
b: BindingTypes.SETUP_LET, b: BindingTypes.SETUP_LET,
c: BindingTypes.SETUP_CONST, c: BindingTypes.LITERAL_CONST,
d: BindingTypes.SETUP_MAYBE_REF, d: BindingTypes.SETUP_MAYBE_REF,
e: BindingTypes.SETUP_LET, e: BindingTypes.SETUP_LET,
foo: BindingTypes.PROPS foo: BindingTypes.PROPS

View File

@ -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)
})
})

View File

@ -43,8 +43,8 @@ describe('sfc props transform', () => {
assertCode(content) assertCode(content)
expect(bindings).toStrictEqual({ expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS, foo: BindingTypes.PROPS,
bar: BindingTypes.SETUP_CONST, bar: BindingTypes.LITERAL_CONST,
hello: BindingTypes.SETUP_CONST hello: BindingTypes.LITERAL_CONST
}) })
}) })
@ -259,7 +259,7 @@ describe('sfc props transform', () => {
expect(() => expect(() =>
compile( compile(
`<script setup> `<script setup>
const x = 1 let x = 1
const { const {
foo = () => x foo = () => x
} = defineProps(['foo']) } = defineProps(['foo'])

View File

@ -111,6 +111,13 @@ export interface SFCScriptCompileOptions {
* options passed to `compiler-dom`. * options passed to `compiler-dom`.
*/ */
templateOptions?: Partial<SFCTemplateCompileOptions> templateOptions?: Partial<SFCTemplateCompileOptions>
/**
* Hoist <script setup> static constants.
* - Only enables when one `<script setup>` exists.
* @default true
*/
hoistStatic?: boolean
} }
export interface ImportBinding { export interface ImportBinding {
@ -144,6 +151,7 @@ export function compileScript(
const enablePropsTransform = !!options.reactivityTransform const enablePropsTransform = !!options.reactivityTransform
const isProd = !!options.isProd const isProd = !!options.isProd
const genSourceMap = options.sourceMap !== false const genSourceMap = options.sourceMap !== false
const hoistStatic = options.hoistStatic !== false && !script
let refBindings: string[] | undefined let refBindings: string[] | undefined
if (!options.id) { if (!options.id) {
@ -743,7 +751,8 @@ export function compileScript(
function checkInvalidScopeReference(node: Node | undefined, method: string) { function checkInvalidScopeReference(node: Node | undefined, method: string) {
if (!node) return if (!node) return
walkIdentifiers(node, id => { walkIdentifiers(node, id => {
if (setupBindings[id.name]) { const binding = setupBindings[id.name]
if (binding && (binding !== BindingTypes.LITERAL_CONST || !hoistStatic)) {
error( error(
`\`${method}()\` in <script setup> cannot reference locally ` + `\`${method}()\` in <script setup> cannot reference locally ` +
`declared variables because it will be hoisted outside of the ` + `declared variables because it will be hoisted outside of the ` +
@ -905,7 +914,7 @@ export function compileScript(
destructured.default.start!, destructured.default.start!,
destructured.default.end! destructured.default.end!
) )
const isLiteral = destructured.default.type.endsWith('Literal') const isLiteral = isLiteralNode(destructured.default)
return isLiteral ? value : `() => (${value})` return isLiteral ? value : `() => (${value})`
} }
} }
@ -1276,14 +1285,21 @@ export function compileScript(
} }
} }
let isAllLiteral = false
// walk declarations to record declared bindings // walk declarations to record declared bindings
if ( if (
(node.type === 'VariableDeclaration' || (node.type === 'VariableDeclaration' ||
node.type === 'FunctionDeclaration' || node.type === 'FunctionDeclaration' ||
node.type === 'ClassDeclaration') && node.type === 'ClassDeclaration' ||
node.type === 'TSEnumDeclaration') &&
!node.declare !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 // walk statements & named exports / variable declarations for top level
@ -1342,17 +1358,13 @@ export function compileScript(
} }
if (isTS) { if (isTS) {
// runtime enum
if (node.type === 'TSEnumDeclaration') {
registerBinding(setupBindings, node.id, BindingTypes.SETUP_CONST)
}
// move all Type declarations to outer scope // move all Type declarations to outer scope
if ( if (
node.type.startsWith('TS') || (node.type.startsWith('TS') ||
(node.type === 'ExportNamedDeclaration' && (node.type === 'ExportNamedDeclaration' &&
node.exportKind === 'type') || node.exportKind === 'type') ||
(node.type === 'VariableDeclaration' && node.declare) (node.type === 'VariableDeclaration' && node.declare)) &&
node.type !== 'TSEnumDeclaration'
) { ) {
recordType(node, declaredTypes) recordType(node, declaredTypes)
hoistNode(node) hoistNode(node)
@ -1474,7 +1486,7 @@ export function compileScript(
) { ) {
helperImports.add(CSS_VARS_HELPER) helperImports.add(CSS_VARS_HELPER)
helperImports.add('unref') helperImports.add('unref')
s.prependRight( s.prependLeft(
startOffset, startOffset,
`\n${genCssVarsCode(cssVars, bindingMetadata, scopeId, isProd)}\n` `\n${genCssVarsCode(cssVars, bindingMetadata, scopeId, isProd)}\n`
) )
@ -1774,9 +1786,17 @@ function walkDeclaration(
node: Declaration, node: Declaration,
bindings: Record<string, BindingTypes>, bindings: Record<string, BindingTypes>,
userImportAliases: Record<string, string> userImportAliases: Record<string, string>
) { ): boolean {
let isAllLiteral = false
if (node.type === 'VariableDeclaration') { if (node.type === 'VariableDeclaration') {
const isConst = node.kind === 'const' const isConst = node.kind === 'const'
isAllLiteral =
isConst &&
node.declarations.every(
decl => decl.id.type === 'Identifier' && isStaticNode(decl.init!)
)
// export const foo = ... // export const foo = ...
for (const { id, init } of node.declarations) { for (const { id, init } of node.declarations) {
const isDefineCall = !!( const isDefineCall = !!(
@ -1789,7 +1809,9 @@ function walkDeclaration(
if (id.type === 'Identifier') { if (id.type === 'Identifier') {
let bindingType let bindingType
const userReactiveBinding = userImportAliases['reactive'] 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 // treat reactive() calls as let since it's meant to be mutable
bindingType = isConst bindingType = isConst
? BindingTypes.SETUP_REACTIVE_CONST ? 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 ( } else if (
node.type === 'TSEnumDeclaration' ||
node.type === 'FunctionDeclaration' || node.type === 'FunctionDeclaration' ||
node.type === 'ClassDeclaration' node.type === 'ClassDeclaration'
) { ) {
@ -1833,6 +1861,8 @@ function walkDeclaration(
// export declarations must be named. // export declarations must be named.
bindings[node.id!.name] = BindingTypes.SETUP_CONST bindings[node.id!.name] = BindingTypes.SETUP_CONST
} }
return isAllLiteral
} }
function walkObjectPattern( function walkObjectPattern(
@ -2138,13 +2168,53 @@ function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
userReactiveImport userReactiveImport
) )
default: default:
if (node.type.endsWith('Literal')) { if (isLiteralNode(node)) {
return true return true
} }
return false 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>` * Analyze bindings in normal `<script>`
* Note that `compileScriptSetup` already analyzes bindings as part of its * Note that `compileScriptSetup` already analyzes bindings as part of its