import { BindingTypes } from '@vue/compiler-core' import { compileSFCScript as compile, assertCode } from './utils' describe('SFC compile `) expect(content).toMatch('return { aa, bb, cc, dd, a, b, c, d, xx, x }') expect(bindings).toStrictEqual({ x: BindingTypes.SETUP_MAYBE_REF, a: BindingTypes.SETUP_LET, b: BindingTypes.SETUP_CONST, c: BindingTypes.SETUP_CONST, d: BindingTypes.SETUP_CONST, xx: BindingTypes.SETUP_MAYBE_REF, aa: BindingTypes.SETUP_LET, bb: BindingTypes.SETUP_CONST, cc: BindingTypes.SETUP_CONST, dd: BindingTypes.SETUP_CONST }) assertCode(content) }) test('binding analysis for destructur', () => { const { content, bindings } = compile(` `) expect(content).toMatch('return { foo, bar, baz, y, z }') expect(bindings).toStrictEqual({ foo: BindingTypes.SETUP_MAYBE_REF, bar: BindingTypes.SETUP_MAYBE_REF, baz: BindingTypes.SETUP_MAYBE_REF, y: BindingTypes.SETUP_MAYBE_REF, z: BindingTypes.SETUP_MAYBE_REF }) assertCode(content) }) test('defineProps()', () => { const { content, bindings } = compile(` `) // should generate working code assertCode(content) // should anayze bindings expect(bindings).toStrictEqual({ foo: BindingTypes.PROPS, bar: BindingTypes.SETUP_CONST, props: BindingTypes.SETUP_CONST }) // should remove defineOptions import and call expect(content).not.toMatch('defineProps') // should generate correct setup signature expect(content).toMatch(`setup(__props, { expose }) {`) // should assign user identifier to it expect(content).toMatch(`const props = __props`) // should include context options in default export expect(content).toMatch(`export default { props: { foo: String },`) }) test('defineProps w/ external definition', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`export default { props: propsModel,`) }) test('defineEmits()', () => { const { content, bindings } = compile(` `) assertCode(content) expect(bindings).toStrictEqual({ myEmit: BindingTypes.SETUP_CONST }) // should remove defineOptions import and call expect(content).not.toMatch('defineEmits') // should generate correct setup signature expect(content).toMatch(`setup(__props, { expose, emit: myEmit }) {`) // should include context options in default export expect(content).toMatch(`export default { emits: ['foo', 'bar'],`) }) test('defineProps/defineEmits in multi-variable declaration', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`const a = 1;`) // test correct removal expect(content).toMatch(`props: ['item'],`) expect(content).toMatch(`emits: ['a'],`) }) test('defineProps/defineEmits in multi-variable declaration (full removal)', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`props: ['item'],`) expect(content).toMatch(`emits: ['a'],`) }) test('defineExpose()', () => { const { content } = compile(` `) assertCode(content) // should remove defineOptions import and call expect(content).not.toMatch('defineExpose') // should generate correct setup signature expect(content).toMatch(`setup(__props, { expose }) {`) // should replace callee expect(content).toMatch(/\bexpose\(\{ foo: 123 \}\)/) }) describe(' `) assertCode(content) }) test('with minimal spaces', () => { const { content } = compile(` `) assertCode(content) }) }) test('script first', () => { const { content } = compile(` `) assertCode(content) }) test('script setup first', () => { const { content } = compile(` `) assertCode(content) }) // #4395 test('script setup first, lang="ts", script block content export default', () => { const { content } = compile(` `) // ensure __default__ is declared before used expect(content).toMatch(/const __default__[\S\s]*\.\.\.__default__/m) assertCode(content) }) }) describe('imports', () => { test('should hoist and expose imports', () => { assertCode( compile(``).content ) }) test('should extract comment for import or type declarations', () => { assertCode( compile(` `).content ) }) // #2740 test('should allow defineProps/Emit at the start of imports', () => { assertCode( compile(``).content ) }) test('dedupe between user & helper', () => { const { content } = compile( ` `, { refSugar: true } ) assertCode(content) expect(content).toMatch(`import { ref } from 'vue'`) }) test('import dedupe between `) assertCode(content) expect(content.indexOf(`import { x }`)).toEqual( content.lastIndexOf(`import { x }`) ) }) }) // in dev mode, declared bindings are returned as an object from setup() // when using TS, users may import types which should not be returned as // values, so we need to check import usage in the template to determine // what to be returned. describe('dev mode import usage check', () => { test('components', () => { const { content } = compile(` `) // FooBar: should not be matched by plain text or incorrect case // FooBaz: used as PascalCase component // FooQux: used as kebab-case component // foo: lowercase component expect(content).toMatch(`return { fooBar, FooBaz, FooQux, foo }`) assertCode(content) }) test('directive', () => { const { content } = compile(` `) expect(content).toMatch(`return { vMyDir }`) assertCode(content) }) test('vue interpolations', () => { const { content } = compile(` `) // x: used in interpolation // y: should not be matched by {{ yy }} or 'y' in binding exps // x$y: #4274 should escape special chars when creating Regex expect(content).toMatch(`return { x, z, x$y }`) assertCode(content) }) // #4340 interpolations in tempalte strings test('js template string interpolations', () => { const { content } = compile(` `) // VAR2 should not be matched expect(content).toMatch(`return { VAR, VAR3 }`) assertCode(content) }) // edge case: last tag in template test('last tag', () => { const { content } = compile(` `) expect(content).toMatch(`return { FooBaz, Last }`) assertCode(content) }) }) describe('inlineTemplate mode', () => { test('should work', () => { const { content } = compile( ` `, { inlineTemplate: true } ) // check snapshot and make sure helper imports and // hoists are placed correctly. assertCode(content) // in inline mode, no need to call expose() since nothing is exposed // anyway! expect(content).not.toMatch(`expose()`) }) test('with defineExpose()', () => { const { content } = compile( ` `, { inlineTemplate: true } ) assertCode(content) expect(content).toMatch(`setup(__props, { expose })`) expect(content).toMatch(`expose({ count })`) }) test('referencing scope components and directives', () => { const { content } = compile( ` `, { inlineTemplate: true } ) expect(content).toMatch('[_unref(vMyDir)]') expect(content).toMatch('_createVNode(ChildComp)') // kebab-case component support expect(content).toMatch('_createVNode(SomeOtherComp)') assertCode(content) }) test('avoid unref() when necessary', () => { // function, const, component import const { content } = compile( ` `, { inlineTemplate: true } ) // no need to unref vue component import expect(content).toMatch(`createVNode(Foo,`) // #2699 should unref named imports from .vue expect(content).toMatch(`unref(bar)`) // should unref other imports expect(content).toMatch(`unref(other)`) // no need to unref constant literals expect(content).not.toMatch(`unref(constant)`) // should directly use .value for known refs expect(content).toMatch(`count.value`) // should unref() on const bindings that may be refs expect(content).toMatch(`unref(maybe)`) // should unref() on let bindings expect(content).toMatch(`unref(lett)`) // no need to unref function declarations expect(content).toMatch(`{ onClick: fn }`) // no need to mark constant fns in patch flag expect(content).not.toMatch(`PROPS`) assertCode(content) }) test('v-model codegen', () => { const { content } = compile( ` `, { inlineTemplate: true } ) // known const ref: set value expect(content).toMatch(`count.value = $event`) // const but maybe ref: also assign .value directly since non-ref // won't work expect(content).toMatch(`maybe.value = $event`) // let: handle both cases expect(content).toMatch( `_isRef(lett) ? lett.value = $event : lett = $event` ) assertCode(content) }) test('template assignment expression codegen', () => { const { content } = compile( ` `, { inlineTemplate: true } ) // known const ref: set value expect(content).toMatch(`count.value = 1`) // const but maybe ref: only assign after check expect(content).toMatch(`maybe.value = count.value`) // let: handle both cases expect(content).toMatch( `_isRef(lett) ? lett.value = count.value : lett = count.value` ) expect(content).toMatch(`_isRef(v) ? v.value += 1 : v += 1`) expect(content).toMatch(`_isRef(v) ? v.value -= 1 : v -= 1`) expect(content).toMatch(`_isRef(v) ? v.value = a : v = a`) expect(content).toMatch(`_isRef(v) ? v.value = _ctx.a : v = _ctx.a`) assertCode(content) }) test('template update expression codegen', () => { const { content } = compile( ` `, { inlineTemplate: true } ) // known const ref: set value expect(content).toMatch(`count.value++`) expect(content).toMatch(`--count.value`) // const but maybe ref (non-ref case ignored) expect(content).toMatch(`maybe.value++`) expect(content).toMatch(`--maybe.value`) // let: handle both cases expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`) expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`) assertCode(content) }) test('template destructure assignment codegen', () => { const { content } = compile( ` `, { inlineTemplate: true } ) // known const ref: set value expect(content).toMatch(`({ count: count.value } = val)`) // const but maybe ref (non-ref case ignored) expect(content).toMatch(`[maybe.value] = val`) // let: assumes non-ref expect(content).toMatch(`{ lett: lett } = val`) assertCode(content) }) test('ssr codegen', () => { const { content } = compile( ` `, { inlineTemplate: true, templateOptions: { ssr: true } } ) expect(content).toMatch(`\n __ssrInlineRender: true,\n`) expect(content).toMatch(`return (_ctx, _push`) expect(content).toMatch(`ssrInterpolate`) assertCode(content) }) }) describe('with TypeScript', () => { test('hoist type declarations', () => { const { content } = compile(` `) assertCode(content) }) test('defineProps/Emit w/ runtime options', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`export default /*#__PURE__*/_defineComponent({ props: { foo: String }, emits: ['a', 'b'], setup(__props, { expose, emit }) {`) }) test('defineProps w/ type', () => { const { content, bindings } = compile(` `) assertCode(content) expect(content).toMatch(`string: { type: String, required: true }`) expect(content).toMatch(`number: { type: Number, required: true }`) expect(content).toMatch(`boolean: { type: Boolean, required: true }`) expect(content).toMatch(`object: { type: Object, required: true }`) expect(content).toMatch(`objectLiteral: { type: Object, required: true }`) expect(content).toMatch(`fn: { type: Function, required: true }`) expect(content).toMatch(`functionRef: { type: Function, required: true }`) expect(content).toMatch(`objectRef: { type: Object, required: true }`) expect(content).toMatch(`dateTime: { type: Date, required: true }`) expect(content).toMatch(`array: { type: Array, required: true }`) expect(content).toMatch(`arrayRef: { type: Array, required: true }`) expect(content).toMatch(`tuple: { type: Array, required: true }`) expect(content).toMatch(`set: { type: Set, required: true }`) expect(content).toMatch(`literal: { type: String, required: true }`) expect(content).toMatch(`optional: { type: null, required: false }`) expect(content).toMatch(`recordRef: { type: Object, required: true }`) expect(content).toMatch(`interface: { type: Object, required: true }`) expect(content).toMatch(`alias: { type: Array, required: true }`) expect(content).toMatch(`method: { type: Function, required: true }`) expect(content).toMatch( `union: { type: [String, Number], required: true }` ) expect(content).toMatch(`literalUnion: { type: String, required: true }`) expect(content).toMatch( `literalUnionNumber: { type: Number, required: true }` ) expect(content).toMatch( `literalUnionMixed: { type: [String, Number, Boolean], required: true }` ) expect(content).toMatch(`intersection: { type: Object, required: true }`) expect(content).toMatch(`foo: { type: [Function, null], required: true }`) expect(bindings).toStrictEqual({ string: BindingTypes.PROPS, number: BindingTypes.PROPS, boolean: BindingTypes.PROPS, object: BindingTypes.PROPS, objectLiteral: BindingTypes.PROPS, fn: BindingTypes.PROPS, functionRef: BindingTypes.PROPS, objectRef: BindingTypes.PROPS, dateTime: BindingTypes.PROPS, array: BindingTypes.PROPS, arrayRef: BindingTypes.PROPS, tuple: BindingTypes.PROPS, set: BindingTypes.PROPS, literal: BindingTypes.PROPS, optional: BindingTypes.PROPS, recordRef: BindingTypes.PROPS, interface: BindingTypes.PROPS, alias: BindingTypes.PROPS, method: BindingTypes.PROPS, union: BindingTypes.PROPS, literalUnion: BindingTypes.PROPS, literalUnionNumber: BindingTypes.PROPS, literalUnionMixed: BindingTypes.PROPS, intersection: BindingTypes.PROPS, foo: BindingTypes.PROPS }) }) test('defineProps w/ interface', () => { const { content, bindings } = compile(` `) assertCode(content) expect(content).toMatch(`x: { type: Number, required: false }`) expect(bindings).toStrictEqual({ x: BindingTypes.PROPS }) }) test('defineProps w/ exported interface', () => { const { content, bindings } = compile(` `) assertCode(content) expect(content).toMatch(`x: { type: Number, required: false }`) expect(bindings).toStrictEqual({ x: BindingTypes.PROPS }) }) test('defineProps w/ exported interface in normal script', () => { const { content, bindings } = compile(` `) assertCode(content) expect(content).toMatch(`x: { type: Number, required: false }`) expect(bindings).toStrictEqual({ x: BindingTypes.PROPS }) }) test('defineProps w/ type alias', () => { const { content, bindings } = compile(` `) assertCode(content) expect(content).toMatch(`x: { type: Number, required: false }`) expect(bindings).toStrictEqual({ x: BindingTypes.PROPS }) }) test('defineProps w/ exported type alias', () => { const { content, bindings } = compile(` `) assertCode(content) expect(content).toMatch(`x: { type: Number, required: false }`) expect(bindings).toStrictEqual({ x: BindingTypes.PROPS }) }) test('withDefaults (static)', () => { const { content, bindings } = compile(` `) assertCode(content) expect(content).toMatch( `foo: { type: String, required: false, default: 'hi' }` ) expect(content).toMatch(`bar: { type: Number, required: false }`) expect(content).toMatch(`baz: { type: Boolean, required: true }`) expect(content).toMatch( `qux: { type: Function, required: false, default() { return 1 } }` ) expect(content).toMatch( `{ foo: string, bar?: number, baz: boolean, qux(): number }` ) expect(content).toMatch(`const props = __props`) expect(bindings).toStrictEqual({ foo: BindingTypes.PROPS, bar: BindingTypes.PROPS, baz: BindingTypes.PROPS, qux: BindingTypes.PROPS, props: BindingTypes.SETUP_CONST }) }) test('withDefaults (dynamic)', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`) expect(content).toMatch( ` _mergeDefaults({ foo: { type: String, required: false }, bar: { type: Number, required: false }, baz: { type: Boolean, required: true } }, { ...defaults })`.trim() ) }) test('defineEmits w/ type', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`) expect(content).toMatch(`emits: ["foo", "bar"]`) }) test('defineEmits w/ type (union)', () => { const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)` expect(() => compile(` `) ).toThrow() }) test('defineEmits w/ type (type literal w/ call signatures)', () => { const type = `{(e: 'foo' | 'bar'): void; (e: 'baz', id: number): void;}` const { content } = compile(` `) assertCode(content) expect(content).toMatch(`emit: (${type}),`) expect(content).toMatch(`emits: ["foo", "bar", "baz"]`) }) test('defineEmits w/ type (interface)', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`) expect(content).toMatch(`emits: ["foo", "bar"]`) }) test('defineEmits w/ type (exported interface)', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`) expect(content).toMatch(`emits: ["foo", "bar"]`) }) test('defineEmits w/ type (type alias)', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`) expect(content).toMatch(`emits: ["foo", "bar"]`) }) test('defineEmits w/ type (exported type alias)', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`) expect(content).toMatch(`emits: ["foo", "bar"]`) }) test('defineEmits w/ type (referenced function type)', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`) expect(content).toMatch(`emits: ["foo", "bar"]`) }) test('defineEmits w/ type (referenced exported function type)', () => { const { content } = compile(` `) assertCode(content) expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`) expect(content).toMatch(`emits: ["foo", "bar"]`) }) test('runtime Enum', () => { const { content, bindings } = compile( `` ) assertCode(content) expect(bindings).toStrictEqual({ Foo: BindingTypes.SETUP_CONST }) }) test('const Enum', () => { const { content, bindings } = compile( `` ) assertCode(content) expect(bindings).toStrictEqual({ Foo: BindingTypes.SETUP_CONST }) }) }) describe('async/await detection', () => { function assertAwaitDetection( code: string, expected: string | ((content: string) => boolean), shouldAsync = true ) { const { content } = compile(``, { refSugar: true }) if (shouldAsync) { expect(content).toMatch(`let __temp, __restore`) } expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`) if (typeof expected === 'string') { expect(content).toMatch(expected) } else { expect(expected(content)).toBe(true) } } test('expression statement', () => { assertAwaitDetection( `await foo`, `;(([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore())` ) }) test('variable', () => { assertAwaitDetection( `const a = 1 + (await foo)`, `1 + ((([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore(),__temp))` ) }) test('ref', () => { assertAwaitDetection( `let a = $ref(1 + (await foo))`, `1 + ((([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore(),__temp))` ) }) test('nested statements', () => { assertAwaitDetection(`if (ok) { await foo } else { await bar }`, code => { return ( code.includes( `;(([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore())` ) && code.includes( `;(([__temp,__restore]=_withAsyncContext(()=>(bar))),__temp=await __temp,__restore())` ) ) }) }) test('should ignore await inside functions', () => { // function declaration assertAwaitDetection( `async function foo() { await bar }`, `await bar`, false ) // function expression assertAwaitDetection( `const foo = async () => { await bar }`, `await bar`, false ) // object method assertAwaitDetection( `const obj = { async method() { await bar }}`, `await bar`, false ) // class method assertAwaitDetection( `const cls = class Foo { async method() { await bar }}`, `await bar`, false ) }) }) describe('errors', () => { test('`) ).toThrow(``) ).toThrow(moduleErrorMsg) expect(() => compile(``) ).toThrow(moduleErrorMsg) expect(() => compile(``) ).toThrow(moduleErrorMsg) }) test('defineProps/Emit() w/ both type and non-type args', () => { expect(() => { compile(``) }).toThrow(`cannot accept both type and non-type arguments`) expect(() => { compile(``) }).toThrow(`cannot accept both type and non-type arguments`) }) test('defineProps/Emit() referencing local var', () => { expect(() => compile(``) ).toThrow(`cannot reference locally declared variables`) expect(() => compile(``) ).toThrow(`cannot reference locally declared variables`) }) test('should allow defineProps/Emit() referencing scope var', () => { assertCode( compile(``).content ) }) test('should allow defineProps/Emit() referencing imported binding', () => { assertCode( compile(``).content ) }) }) }) describe('SFC analyze `) expect(scriptAst).toBeDefined() }) it('recognizes props array declaration', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: BindingTypes.PROPS, bar: BindingTypes.PROPS }) expect(bindings!.__isScriptSetup).toBe(false) }) it('recognizes props object declaration', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: BindingTypes.PROPS, bar: BindingTypes.PROPS, baz: BindingTypes.PROPS, qux: BindingTypes.PROPS }) expect(bindings!.__isScriptSetup).toBe(false) }) it('recognizes setup return', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: BindingTypes.SETUP_MAYBE_REF, bar: BindingTypes.SETUP_MAYBE_REF }) expect(bindings!.__isScriptSetup).toBe(false) }) it('recognizes async setup return', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: BindingTypes.SETUP_MAYBE_REF, bar: BindingTypes.SETUP_MAYBE_REF }) expect(bindings!.__isScriptSetup).toBe(false) }) it('recognizes data return', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: BindingTypes.DATA, bar: BindingTypes.DATA }) }) it('recognizes methods', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: BindingTypes.OPTIONS }) }) it('recognizes computeds', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: BindingTypes.OPTIONS, bar: BindingTypes.OPTIONS }) }) it('recognizes injections array declaration', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: BindingTypes.OPTIONS, bar: BindingTypes.OPTIONS }) }) it('recognizes injections object declaration', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: BindingTypes.OPTIONS, bar: BindingTypes.OPTIONS }) }) it('works for mixed bindings', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ foo: BindingTypes.OPTIONS, bar: BindingTypes.PROPS, baz: BindingTypes.SETUP_MAYBE_REF, qux: BindingTypes.DATA, quux: BindingTypes.OPTIONS, quuz: BindingTypes.OPTIONS }) }) it('works for script setup', () => { const { bindings } = compile(` `) expect(bindings).toStrictEqual({ r: BindingTypes.SETUP_CONST, a: BindingTypes.SETUP_REF, b: BindingTypes.SETUP_LET, c: BindingTypes.SETUP_CONST, d: BindingTypes.SETUP_MAYBE_REF, e: BindingTypes.SETUP_LET, foo: BindingTypes.PROPS }) }) })