/**
* 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 = {
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
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`
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)
.join('')
}
}
+
+function isConst(type: unknown) {
+ return (
+ type === BindingTypes.SETUP_CONST ||
+ type === BindingTypes.LITERAL_CONST ||
+ type === BindingTypes.SETUP_REACTIVE_CONST
+ )
+}
// 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 }
}
`;
exports[`SFC compile <script setup> > defineProps() 1`] = `
-"export default {
+"const bar = 1
+
+export default {
props: {
foo: String
},
const props = __props;
-const bar = 1
return { props, bar }
}
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 } }
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 } }
}
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 } }
}
`;
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
setup(__props, { expose }) {
expose();
- const bar = 1
exports[`SFC compile <script setup> > with TypeScript > runtime Enum in normal script 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
-enum Foo { A = 123 }
-
+
export enum D { D = \\"D\\" }
const enum C { C = \\"C\\" }
enum B { B = \\"B\\" }
setup(__props, { expose }) {
expose();
+ enum Foo { A = 123 }
return { D, C, B, Foo }
}
--- /dev/null
+// 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 () => {}
+}
+
+}"
+`;
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();
_useCssVars(_ctx => ({
\\"xxxxxxxx-width\\": (width)
}))
-const color = 'red';const width = 100
+
return { color, width }
}
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();
_useCssVars(_ctx => ({
\\"xxxxxxxx-color\\": (color)
}))
-const color = 'red'
+
return { color }
}
exports[`CSS vars injection > codegen > w/ <script setup> using the same var multiple times 1`] = `
"import { useCssVars as _useCssVars, unref as _unref } from 'vue'
-
+const color = 'red'
+
export default {
setup(__props, { expose }) {
expose();
\\"xxxxxxxx-color\\": (color)
}))
- const color = 'red'
return { color }
}
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: {
\\"xxxxxxxx-foo\\": (__props.foo)
}))
- const color = 'red'
const size = ref('10px')
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
})
// should analyze bindings
expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS,
- bar: BindingTypes.SETUP_CONST,
+ bar: BindingTypes.LITERAL_CONST,
props: BindingTypes.SETUP_REACTIVE_CONST
})
)
assertCode(content)
expect(bindings).toStrictEqual({
- Foo: BindingTypes.SETUP_CONST
+ Foo: BindingTypes.LITERAL_CONST
})
})
)
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
})
})
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
})
})
test('defineProps/Emit() referencing local var', () => {
expect(() =>
compile(`<script setup>
- const bar = 1
+ let bar = 1
defineProps({
foo: {
default: () => bar
expect(() =>
compile(`<script setup>
- const bar = 'hello'
+ let bar = 'hello'
defineEmits([bar])
</script>`)
).toThrow(`cannot reference locally declared variables`)
</script>
`)
expect(bindings).toStrictEqual({
- foo: BindingTypes.SETUP_CONST
+ foo: BindingTypes.LITERAL_CONST
})
})
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
--- /dev/null
+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)
+ })
+})
assertCode(content)
expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS,
- bar: BindingTypes.SETUP_CONST,
- hello: BindingTypes.SETUP_CONST
+ bar: BindingTypes.LITERAL_CONST,
+ hello: BindingTypes.LITERAL_CONST
})
})
expect(() =>
compile(
`<script setup>
- const x = 1
+ let x = 1
const {
foo = () => x
} = defineProps(['foo'])
* 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 {
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) {
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 ` +
destructured.default.start!,
destructured.default.end!
)
- const isLiteral = destructured.default.type.endsWith('Literal')
+ const isLiteral = isLiteralNode(destructured.default)
return isLiteral ? value : `() => (${value})`
}
}
}
}
+ 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
}
if (isTS) {
- // runtime enum
- if (node.type === 'TSEnumDeclaration') {
- registerBinding(setupBindings, node.id, BindingTypes.SETUP_CONST)
- }
-
// move all Type declarations to outer scope
if (
- node.type.startsWith('TS') ||
- (node.type === 'ExportNamedDeclaration' &&
- node.exportKind === 'type') ||
- (node.type === 'VariableDeclaration' && node.declare)
+ (node.type.startsWith('TS') ||
+ (node.type === 'ExportNamedDeclaration' &&
+ node.exportKind === 'type') ||
+ (node.type === 'VariableDeclaration' && node.declare)) &&
+ node.type !== 'TSEnumDeclaration'
) {
recordType(node, declaredTypes)
hoistNode(node)
) {
helperImports.add(CSS_VARS_HELPER)
helperImports.add('unref')
- s.prependRight(
+ s.prependLeft(
startOffset,
`\n${genCssVarsCode(cssVars, bindingMetadata, scopeId, isProd)}\n`
)
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 = !!(
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
}
}
}
+ } 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'
) {
// export declarations must be named.
bindings[node.id!.name] = BindingTypes.SETUP_CONST
}
+
+ return isAllLiteral
}
function walkObjectPattern(
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