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
|
* 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 = {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`] = `
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
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'])
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue