mirror of https://github.com/vuejs/core.git
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:
parent
e224922e97
commit
7def8b15b8
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,7 +1725,6 @@ 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\\" }
|
||||
|
@ -1732,6 +1734,7 @@ export default /*#__PURE__*/_defineComponent({
|
|||
setup(__props, { expose }) {
|
||||
expose();
|
||||
|
||||
enum Foo { A = 123 }
|
||||
|
||||
return { D, C, B, Foo }
|
||||
}
|
||||
|
|
|
@ -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 () => {}
|
||||
}
|
||||
|
||||
}"
|
||||
`;
|
|
@ -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,6 +109,7 @@ 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 }) {
|
||||
|
@ -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')
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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'])
|
||||
|
|
|
@ -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.startsWith('TS') ||
|
||||
(node.type === 'ExportNamedDeclaration' &&
|
||||
node.exportKind === 'type') ||
|
||||
(node.type === 'VariableDeclaration' && node.declare)
|
||||
(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
|
||||
|
|
Loading…
Reference in New Issue