import { vi } from 'vitest'
import { BindingTypes } from '@vue/compiler-core'
import {
assertCode,
compileSFCScript as compile,
getPositionInCode,
mockId,
} from './utils'
import { type RawSourceMap, SourceMapConsumer } from 'source-map-js'
vi.mock('../src/warn', () => ({
warn: vi.fn(),
warnOnce: vi.fn(),
}))
import { warnOnce } from '../src/warn'
const warnOnceMock = vi.mocked(warnOnce)
describe('SFC compile
`)
expect(content).toMatch(`return { a, b }`)
assertCode(content)
})
test('should expose top level declarations', () => {
const { content, bindings } = compile(`
`)
expect(content).toMatch(
`return { get aa() { return aa }, set aa(v) { aa = v }, ` +
`bb, cc, dd, get a() { return a }, set a(v) { a = v }, b, c, d, ` +
`get xx() { return xx }, get x() { return 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.LITERAL_CONST,
cc: BindingTypes.SETUP_CONST,
dd: BindingTypes.SETUP_CONST,
})
assertCode(content)
})
test('binding analysis for destructure', () => {
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('demote const reactive binding to let when used in v-model', () => {
warnOnceMock.mockClear()
const { content, bindings } = compile(`
`)
expect(content).toMatch(
`let name = reactive({ first: 'john', last: 'doe' })`,
)
expect(bindings!.name).toBe(BindingTypes.SETUP_LET)
expect(warnOnceMock).toHaveBeenCalledTimes(1)
expect(warnOnceMock).toHaveBeenCalledWith(
expect.stringContaining(
'`v-model` cannot update a `const` reactive binding',
),
)
assertCode(content)
})
test('demote const reactive binding to let when used in v-model (inlineTemplate)', () => {
warnOnceMock.mockClear()
const { content, bindings } = compile(
`
`,
{ inlineTemplate: true },
)
expect(content).toMatch(
`let name = reactive({ first: 'john', last: 'doe' })`,
)
expect(bindings!.name).toBe(BindingTypes.SETUP_LET)
expect(warnOnceMock).toHaveBeenCalledTimes(1)
expect(warnOnceMock).toHaveBeenCalledWith(
expect.stringContaining(
'`v-model` cannot update a `const` reactive binding',
),
)
assertCode(content)
})
test('v-model should error on literal const bindings', () => {
expect(() =>
compile(
`
`,
{ inlineTemplate: true },
),
).toThrow('v-model cannot be used on a const binding')
})
describe('
`)
assertCode(content)
})
test('script setup first', () => {
const { content } = compile(`
`)
assertCode(content)
})
// #7805
test('keep original semi style', () => {
const { content } = compile(`
`)
assertCode(content)
expect(content).toMatch(`console.log('test')`)
expect(content).toMatch(`const props = __props;`)
expect(content).toMatch(`const emit = __emit;`)
expect(content).toMatch(`(function () {})()`)
})
test('script setup first, named default export', () => {
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('spaces in ExportDefaultDeclaration node', () => {
// #4371
test('with many spaces and newline', () => {
// #4371
const { content } = compile(`
`)
assertCode(content)
})
test('with minimal spaces', () => {
const { content } = compile(`
`)
assertCode(content)
})
})
test('export call expression as default', () => {
const { content } = compile(`
`)
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(
`
`,
)
assertCode(content)
expect(content).toMatch(
`import { useCssVars as _useCssVars, unref as _unref } from 'vue'`,
)
expect(content).toMatch(`import { useCssVars, ref } from 'vue'`)
})
test('import dedupe between
`)
assertCode(content)
expect(content.indexOf(`import { x }`)).toEqual(
content.lastIndexOf(`import { x }`),
)
})
describe('import ref/reactive function from other place', () => {
test('import directly', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
ref: BindingTypes.SETUP_MAYBE_REF,
reactive: BindingTypes.SETUP_MAYBE_REF,
foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_MAYBE_REF,
})
})
test('import w/ alias', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
_reactive: BindingTypes.SETUP_MAYBE_REF,
_ref: BindingTypes.SETUP_MAYBE_REF,
foo: BindingTypes.SETUP_MAYBE_REF,
bar: BindingTypes.SETUP_MAYBE_REF,
})
})
test('aliased usage before import site', () => {
const { bindings } = compile(`
`)
expect(bindings).toStrictEqual({
bar: BindingTypes.SETUP_REACTIVE_CONST,
x: BindingTypes.SETUP_CONST,
})
})
})
test('should support module string names syntax', () => {
const { content, bindings } = compile(`
`)
assertCode(content)
expect(bindings).toStrictEqual({
foo: BindingTypes.SETUP_MAYBE_REF,
})
})
})
describe('inlineTemplate mode', () => {
test('should work', () => {
const { content } = compile(
`
{{ count }}
static
`,
{ 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: __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(
`
{{ bar }}
{{ count }} {{ constant }} {{ maybe }} {{ lett }} {{ other }}
{{ tree.foo() }}
`,
{ 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 namespace import (this also preserves tree-shaking)
expect(content).toMatch(`tree.foo()`)
// 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: assign if ref, otherwise do nothing
expect(content).toMatch(`_isRef(maybe) ? (maybe).value = $event : null`)
// let: handle both cases
expect(content).toMatch(
`_isRef(lett) ? (lett).value = $event : lett = $event`,
)
assertCode(content)
})
test('v-model w/ newlines codegen', () => {
const { content } = compile(
`
`,
{ inlineTemplate: true },
)
expect(content).toMatch(`_isRef(count) ? (count).value = $event : null`)
assertCode(content)
})
test('v-model should not generate ref assignment code for non-setup bindings', () => {
const { content } = compile(
`
`,
{ inlineTemplate: true },
)
expect(content).not.toMatch(`_isRef(foo)`)
})
test('template assignment expression codegen', () => {
const { content } = compile(
`
{
let a = '' + lett
v = a
}"/>
{
// nested scopes
(()=>{
let x = a
(()=>{
let z = x
let z2 = z
})
let lz = z
})
v = a
}"/>
`,
{ 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(
`