diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index 1221772f706..65bbcb36dd6 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -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 = { diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 4bbe891e209..52612edd612 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -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 diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index 43c69559688..112dc63cb7a 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -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 + ) +} diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap index 2813a0be788..4615b20fe73 100644 --- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap +++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap @@ -1,11 +1,12 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`SFC analyze ` + `, + { 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(``) ).toThrow(`cannot reference locally declared variables`) @@ -1785,7 +1786,7 @@ describe('SFC analyze `) expect(bindings).toStrictEqual({ - foo: BindingTypes.SETUP_CONST + foo: BindingTypes.LITERAL_CONST }) }) @@ -1951,7 +1952,7 @@ describe('SFC analyze + `) + + // 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(` + + `) + // 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(` + + `) + + // 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(` + + `) + 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(` + + `) + 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(` + + `) + 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(` + + `) + 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(` + + + `) + expect(bindings).toStrictEqual({ + foo: BindingTypes.LITERAL_CONST + }) + assertCode(content) + }) +}) diff --git a/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts b/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts index 05c7989b8f1..5401a9e0305 100644 --- a/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts @@ -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( `