Skip to content

Commit

Permalink
fix(compiler-core): warn when slot used on non-root template
Browse files Browse the repository at this point in the history
  • Loading branch information
Disservin committed Aug 8, 2024
1 parent 28db2e6 commit bdfdb9c
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 4 deletions.
214 changes: 214 additions & 0 deletions packages/compiler-core/__tests__/__snapshots__/parse.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,220 @@ exports[`compiler: parse > Edge Cases > valid html 1`] = `
}
`;
exports[`compiler: parse > Edge Cases > warn when v-slot used on non-root <template> 1`] = `
{
"cached": 0,
"children": [
{
"children": [
{
"children": [
{
"children": [
{
"content": " Header ",
"loc": {
"end": {
"column": 41,
"line": 4,
"offset": 93,
},
"source": " Header ",
"start": {
"column": 33,
"line": 4,
"offset": 85,
},
},
"type": 2,
},
],
"codegenNode": undefined,
"loc": {
"end": {
"column": 52,
"line": 4,
"offset": 104,
},
"source": "<template #header> Header </template>",
"start": {
"column": 15,
"line": 4,
"offset": 67,
},
},
"ns": 0,
"props": [
{
"arg": {
"constType": 3,
"content": "header",
"isStatic": true,
"loc": {
"end": {
"column": 32,
"line": 4,
"offset": 84,
},
"source": "header",
"start": {
"column": 26,
"line": 4,
"offset": 78,
},
},
"type": 4,
},
"exp": undefined,
"loc": {
"end": {
"column": 32,
"line": 4,
"offset": 84,
},
"source": "#header",
"start": {
"column": 25,
"line": 4,
"offset": 77,
},
},
"modifiers": [],
"name": "slot",
"rawName": "#header",
"type": 7,
},
],
"tag": "template",
"tagType": 3,
"type": 1,
},
],
"codegenNode": undefined,
"loc": {
"end": {
"column": 24,
"line": 5,
"offset": 128,
},
"source": "<template v-if="true">
<template #header> Header </template>
</template>",
"start": {
"column": 13,
"line": 3,
"offset": 30,
},
},
"ns": 0,
"props": [
{
"arg": undefined,
"exp": {
"constType": 0,
"content": "true",
"isStatic": false,
"loc": {
"end": {
"column": 33,
"line": 3,
"offset": 50,
},
"source": "true",
"start": {
"column": 29,
"line": 3,
"offset": 46,
},
},
"type": 4,
},
"loc": {
"end": {
"column": 34,
"line": 3,
"offset": 51,
},
"source": "v-if="true"",
"start": {
"column": 23,
"line": 3,
"offset": 40,
},
},
"modifiers": [],
"name": "if",
"rawName": "v-if",
"type": 7,
},
],
"tag": "template",
"tagType": 3,
"type": 1,
},
],
"codegenNode": undefined,
"loc": {
"end": {
"column": 18,
"line": 6,
"offset": 146,
},
"source": "<Comp>
<template v-if="true">
<template #header> Header </template>
</template>
</Comp>",
"start": {
"column": 11,
"line": 2,
"offset": 11,
},
},
"ns": 0,
"props": [],
"tag": "Comp",
"tagType": 1,
"type": 1,
},
],
"codegenNode": undefined,
"components": [],
"directives": [],
"helpers": Set {},
"hoists": [],
"imports": [],
"loc": {
"end": {
"column": 11,
"line": 7,
"offset": 157,
},
"source": "
<Comp>
<template v-if="true">
<template #header> Header </template>
</template>
</Comp>
",
"start": {
"column": 1,
"line": 1,
"offset": 0,
},
},
"source": "
<Comp>
<template v-if="true">
<template #header> Header </template>
</template>
</Comp>
",
"temps": 0,
"type": 0,
}
`;
exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > <template><![CDATA[cdata]]></template> 1`] = `
{
"cached": 0,
Expand Down
36 changes: 36 additions & 0 deletions packages/compiler-core/__tests__/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1992,6 +1992,42 @@ describe('compiler: parse', () => {
expect(ast).toMatchSnapshot()
})

test('warn when v-slot used on non-root <template>', () => {
const source = `
<Comp>
<template v-if="true">
<template #header> Header </template>
</template>
</Comp>
`

expect(() => {
baseParse(source)
}).toThrow(
'<template v-slot> can only appear at the root level inside the receiving component',
)

const spy = vi.fn()
const ast = baseParse(source, {
onError: spy,
})

expect(spy.mock.calls).toMatchObject([
[
{
code: ErrorCodes.X_TEMPLATE_NOT_ROOT,
loc: {
start: { column: 15, line: 4, offset: 67 },
end: { column: 15, line: 4, offset: 67 },
source: '',
},
},
],
])

expect(ast).toMatchSnapshot()
})

test('parse with correct location info', () => {
const fooSrc = `foo\n is `
const barSrc = `{{ bar }}`
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler-core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export enum ErrorCodes {
X_MISSING_INTERPOLATION_END,
X_MISSING_DIRECTIVE_NAME,
X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END,
X_TEMPLATE_NOT_ROOT,

// transform errors
X_V_IF_NO_EXPRESSION,
Expand Down Expand Up @@ -151,6 +152,9 @@ export const errorMessages: Record<ErrorCodes, string> = {
'End bracket for dynamic directive argument was not found. ' +
'Note that dynamic directive argument cannot contain spaces.',
[ErrorCodes.X_MISSING_DIRECTIVE_NAME]: 'Legal directive name was expected.',
[ErrorCodes.X_TEMPLATE_NOT_ROOT]:
`<template v-slot> can only appear at the root level inside ` +
`the receiving component`,

// transform errors
[ErrorCodes.X_V_IF_NO_EXPRESSION]: `v-if/v-else-if is missing expression.`,
Expand Down
12 changes: 9 additions & 3 deletions packages/compiler-core/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) {
if (!inVPre) {
if (tag === 'slot') {
el.tagType = ElementTypes.SLOT
} else if (isFragmentTemplate(el)) {
} else if (isFragmentTemplate(el, stack[0])) {
el.tagType = ElementTypes.TEMPLATE
} else if (isComponent(el)) {
el.tagType = ElementTypes.COMPONENT
Expand Down Expand Up @@ -698,7 +698,7 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) {
currentOptions,
) &&
el.tag === 'template' &&
!isFragmentTemplate(el)
!isFragmentTemplate(el, stack[0])
) {
__DEV__ &&
warnDeprecation(
Expand Down Expand Up @@ -749,13 +749,19 @@ function backTrack(index: number, c: number) {
}

const specialTemplateDir = new Set(['if', 'else', 'else-if', 'for', 'slot'])
function isFragmentTemplate({ tag, props }: ElementNode): boolean {
function isFragmentTemplate(
{ tag, props, loc }: ElementNode,
parent: ElementNode,
): boolean {
if (tag === 'template') {
for (let i = 0; i < props.length; i++) {
if (
props[i].type === NodeTypes.DIRECTIVE &&
specialTemplateDir.has((props[i] as DirectiveNode).name)
) {
if (props[i].name === 'slot' && parent && !isComponent(parent)) {
emitError(ErrorCodes.X_TEMPLATE_NOT_ROOT, loc.start.offset)
}
return true
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-dom/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function createDOMCompilerError(
}

export enum DOMErrorCodes {
X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */,
X_V_HTML_NO_EXPRESSION = 54 /* ErrorCodes.__EXTEND_POINT__ */,
X_V_HTML_WITH_CHILDREN,
X_V_TEXT_NO_EXPRESSION,
X_V_TEXT_WITH_CHILDREN,
Expand Down

0 comments on commit bdfdb9c

Please sign in to comment.