vue3-core/packages/compiler-sfc/__tests__/compileTemplate.spec.ts

547 lines
13 KiB
TypeScript

import { type RawSourceMap, SourceMapConsumer } from 'source-map-js'
import { parse as babelParse } from '@babel/parser'
import {
type SFCTemplateCompileOptions,
compileTemplate,
} from '../src/compileTemplate'
import { type SFCTemplateBlock, parse } from '../src/parse'
import { compileScript } from '../src'
function compile(opts: Omit<SFCTemplateCompileOptions, 'id'>) {
return compileTemplate({
...opts,
id: '',
})
}
test('should work', () => {
const source = `<div><p>{{ render }}</p></div>`
const result = compile({ filename: 'example.vue', source })
expect(result.errors.length).toBe(0)
expect(result.source).toBe(source)
// should expose render fn
expect(result.code).toMatch(`export function render(`)
})
// #6807
test('should work with style comment', () => {
const source = `
<div style="
/* nothing */
width: 300px;
height: 100px/* nothing */
">{{ render }}</div>
`
const result = compile({ filename: 'example.vue', source })
expect(result.errors.length).toBe(0)
expect(result.source).toBe(source)
expect(result.code).toMatch(`{"width":"300px","height":"100px"}`)
})
test('preprocess pug', () => {
const template = parse(
`
<template lang="pug">
body
h1 Pug Examples
div.container
p Cool Pug example!
</template>
`,
{ filename: 'example.vue', sourceMap: true },
).descriptor.template as SFCTemplateBlock
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang,
})
expect(result.errors.length).toBe(0)
})
test('preprocess pug with indents and blank lines', () => {
const template = parse(
`
<template lang="pug">
body
h1 The next line contains four spaces.
div.container
p The next line is empty.
p This is the last line.
</template>
`,
{ filename: 'example.vue', sourceMap: true },
).descriptor.template as SFCTemplateBlock
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang,
})
expect(result.errors.length).toBe(0)
expect(result.source).toBe(
'<body><h1>The next line contains four spaces.</h1><div class="container"><p>The next line is empty.</p></div><p>This is the last line.</p></body>',
)
})
test('warn missing preprocessor', () => {
const template = parse(`<template lang="unknownLang">hi</template>\n`, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template as SFCTemplateBlock
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang,
})
expect(result.errors.length).toBe(1)
})
test('transform asset url options', () => {
const input = { source: `<foo bar="~baz"/>`, filename: 'example.vue' }
// Object option
const { code: code1 } = compile({
...input,
transformAssetUrls: {
tags: { foo: ['bar'] },
},
})
expect(code1).toMatch(`import _imports_0 from 'baz'\n`)
// legacy object option (direct tags config)
const { code: code2 } = compile({
...input,
transformAssetUrls: {
foo: ['bar'],
},
})
expect(code2).toMatch(`import _imports_0 from 'baz'\n`)
// false option
const { code: code3 } = compile({
...input,
transformAssetUrls: false,
})
expect(code3).not.toMatch(`import _imports_0 from 'baz'\n`)
})
test('source map', () => {
const template = parse(
`
<template>
<div><p>{{ foobar }}</p></div>
</template>
`,
{ filename: 'example.vue', sourceMap: true },
).descriptor.template!
const { code, map } = compile({
filename: 'example.vue',
source: template.content,
})
expect(map!.sources).toEqual([`example.vue`])
expect(map!.sourcesContent).toEqual([template.content])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, 'foobar')),
).toMatchObject(getPositionInCode(template.content, `foobar`))
})
test('source map: v-if generated comment should not have original position', () => {
const template = parse(
`
<template>
<div v-if="true"></div>
</template>
`,
{ filename: 'example.vue', sourceMap: true },
).descriptor.template!
const { code, map } = compile({
filename: 'example.vue',
source: template.content,
})
expect(map!.sources).toEqual([`example.vue`])
expect(map!.sourcesContent).toEqual([template.content])
const consumer = new SourceMapConsumer(map as RawSourceMap)
const commentNode = code.match(/_createCommentVNode\("v-if", true\)/)
expect(commentNode).not.toBeNull()
const commentPosition = getPositionInCode(code, commentNode![0])
const originalPosition = consumer.originalPositionFor(commentPosition)
// the comment node should not be mapped to the original source
expect(originalPosition.column).toBeNull()
expect(originalPosition.line).toBeNull()
expect(originalPosition.source).toBeNull()
})
test('should work w/ AST from descriptor', () => {
const source = `
<template>
<div><p>{{ foobar }}</p></div>
</template>
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template!
expect(template.ast!.source).toBe(source)
const { code, map } = compile({
filename: 'example.vue',
source: template.content,
ast: template.ast,
})
expect(map!.sources).toEqual([`example.vue`])
// when reusing AST from SFC parse for template compile,
// the source corresponds to the entire SFC
expect(map!.sourcesContent).toEqual([source])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, 'foobar')),
).toMatchObject(getPositionInCode(source, `foobar`))
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content,
}).code,
)
})
test('should work w/ AST from descriptor in SSR mode', () => {
const source = `
<template>
<div><p>{{ foobar }}</p></div>
</template>
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template!
expect(template.ast!.source).toBe(source)
const { code, map } = compile({
filename: 'example.vue',
source: '', // make sure it's actually using the AST instead of source
ast: template.ast,
ssr: true,
})
expect(map!.sources).toEqual([`example.vue`])
// when reusing AST from SFC parse for template compile,
// the source corresponds to the entire SFC
expect(map!.sourcesContent).toEqual([source])
const consumer = new SourceMapConsumer(map as RawSourceMap)
expect(
consumer.originalPositionFor(getPositionInCode(code, 'foobar')),
).toMatchObject(getPositionInCode(source, `foobar`))
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content,
ssr: true,
}).code,
)
})
test('should not reuse AST if using custom compiler', () => {
const source = `
<template>
<div><p>{{ foobar }}</p></div>
</template>
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template!
const { code } = compile({
filename: 'example.vue',
source: template.content,
ast: template.ast,
compiler: {
parse: () => null as any,
// @ts-expect-error
compile: input => ({ code: input }),
},
})
// what we really want to assert is that the `input` received by the custom
// compiler is the source string, not the AST.
expect(code).toBe(template.content)
})
test('should force re-parse on already transformed AST', () => {
const source = `
<template>
<div><p>{{ foobar }}</p></div>
</template>
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template!
// force set to empty, if this is reused then it won't generate proper code
template.ast!.children = []
template.ast!.transformed = true
const { code } = compile({
filename: 'example.vue',
source: '',
ast: template.ast,
})
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content,
}).code,
)
})
test('should force re-parse with correct compiler in SSR mode', () => {
const source = `
<template>
<div><p>{{ foobar }}</p></div>
</template>
`
const template = parse(source, {
filename: 'example.vue',
sourceMap: true,
}).descriptor.template!
// force set to empty, if this is reused then it won't generate proper code
template.ast!.children = []
template.ast!.transformed = true
const { code } = compile({
filename: 'example.vue',
source: '',
ast: template.ast,
ssr: true,
})
expect(code).toBe(
compile({
filename: 'example.vue',
source: template.content,
ssr: true,
}).code,
)
})
test('template errors', () => {
const result = compile({
filename: 'example.vue',
source: `<div
:bar="a[" v-model="baz"/>`,
})
expect(result.errors).toMatchSnapshot()
})
test('preprocessor errors', () => {
const template = parse(
`
<template lang="pug">
div(class='class)
</template>
`,
{ filename: 'example.vue', sourceMap: true },
).descriptor.template as SFCTemplateBlock
const result = compile({
filename: 'example.vue',
source: template.content,
preprocessLang: template.lang,
})
expect(result.errors.length).toBe(1)
const message = result.errors[0].toString()
expect(message).toMatch(`Error: example.vue:3:1`)
expect(message).toMatch(
`The end of the string reached with no closing bracket ) found.`,
)
})
// #3447
test('should generate the correct imports expression', () => {
const { code } = compile({
filename: 'example.vue',
source: `
<img src="./foo.svg"/>
<Comp>
<img src="./bar.svg"/>
</Comp>
`,
ssr: true,
})
expect(code).toMatch(`_ssrRenderAttr(\"src\", _imports_1)`)
expect(code).toMatch(`_createVNode(\"img\", { src: _imports_1 })`)
})
// #3874
test('should not hoist srcset URLs in SSR mode', () => {
const { code } = compile({
filename: 'example.vue',
source: `
<picture>
<source srcset="./img/foo.svg"/>
<img src="./img/foo.svg"/>
</picture>
<router-link>
<picture>
<source srcset="./img/bar.svg"/>
<img src="./img/bar.svg"/>
</picture>
</router-link>
`,
ssr: true,
})
expect(code).toMatchSnapshot()
})
// #6742
test('dynamic v-on + static v-on should merged', () => {
const source = `<input @blur="onBlur" @[validateEvent]="onValidateEvent">`
const result = compile({ filename: 'example.vue', source })
expect(result.code).toMatchSnapshot()
})
// #9853 regression found in Nuxt tests
// walkIdentifiers can get called multiple times on the same node
// due to #9729 calling it during SFC template usage check.
// conditions needed:
// 1. `<script setup lang="ts">`
// 2. Has import
// 3. inlineTemplate: false
// 4. AST being reused
test('prefixing edge case for reused AST', () => {
const src = `
<script setup lang="ts">
import { Foo } from './foo'
</script>
<template>
{{ list.map((t, index) => ({ t: t })) }}
</template>
`
const { descriptor } = parse(src)
// compileScript triggers importUsageCheck
compileScript(descriptor, { id: 'xxx' })
const { code } = compileTemplate({
id: 'xxx',
filename: 'test.vue',
ast: descriptor.template!.ast,
source: descriptor.template!.content,
})
expect(code).not.toMatch(`_ctx.t`)
})
test('prefixing edge case for reused AST ssr mode', () => {
const src = `
<script setup lang="ts">
import { Foo } from './foo'
</script>
<template>
<Bar>
<template #option="{ foo }"></template>
</Bar>
</template>
`
const { descriptor } = parse(src)
// compileScript triggers importUsageCheck
compileScript(descriptor, { id: 'xxx' })
expect(() =>
compileTemplate({
id: 'xxx',
filename: 'test.vue',
ast: descriptor.template!.ast,
source: descriptor.template!.content,
ssr: true,
}),
).not.toThrowError()
})
// #10852
test('non-identifier expression in legacy filter syntax', () => {
const src = `
<template>
<div>
Today is
{{ new Date() | formatDate }}
</div>
</template>
`
const { descriptor } = parse(src)
const compilationResult = compileTemplate({
id: 'xxx',
filename: 'test.vue',
ast: descriptor.template!.ast,
source: descriptor.template!.content,
ssr: false,
compilerOptions: {
compatConfig: {
MODE: 2,
},
},
})
expect(() => {
babelParse(compilationResult.code, { sourceType: 'module' })
}).not.toThrow()
})
interface Pos {
line: number
column: number
name?: string
}
function getPositionInCode(
code: string,
token: string,
expectName: string | boolean = false,
): Pos {
const generatedOffset = code.indexOf(token)
let line = 1
let lastNewLinePos = -1
for (let i = 0; i < generatedOffset; i++) {
if (code.charCodeAt(i) === 10 /* newline char code */) {
line++
lastNewLinePos = i
}
}
const res: Pos = {
line,
column:
lastNewLinePos === -1
? generatedOffset
: generatedOffset - lastNewLinePos - 1,
}
if (expectName) {
res.name = typeof expectName === 'string' ? expectName : token
}
return res
}