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
Implements the warning from vue2 in vue3.

fixes #11521
  • Loading branch information
Disservin committed Aug 16, 2024
1 parent d13095b commit 27ed27e
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 91 deletions.
72 changes: 0 additions & 72 deletions packages/compiler-core/__tests__/transform.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { transformElement } from '../src/transforms/transformElement'
import { transformSlotOutlet } from '../src/transforms/transformSlotOutlet'
import { transformText } from '../src/transforms/transformText'
import { PatchFlags } from '@vue/shared'
import type { CompilerOptions } from '@vue/compiler-core'

describe('compiler: transform', () => {
test('context state', () => {
Expand Down Expand Up @@ -379,75 +378,4 @@ describe('compiler: transform', () => {
)
})
})

describe('errors', () => {
function transformWithCodegen(
template: string,
options: CompilerOptions = {},
) {
const ast = baseParse(template)
transform(ast, {
nodeTransforms: [
transformIf,
transformFor,
transformText,
transformSlotOutlet,
transformElement,
],
...options,
})
return ast
}

test('warn when v-slot used on non-root level <template>', () => {
const onError = vi.fn()

const ast = transformWithCodegen(
`
<template>
<Bar>
<template>
<template #header> Header </template>
</template>
</Bar>
</template>`,
{ onError },
)

expect(onError.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_TEMPLATE_NOT_ROOT,
loc: (ast.children[0] as any).children[0].children[0].children[0].loc,
},
])
})

test('dont warn when v-slot used on root level <template>', () => {
const onError = vi.fn()

transformWithCodegen(
`<template>
<Comp>
<template #header> Header </template>
</Comp>
</template>`,
{ onError },
)

expect(onError.mock.calls).toEqual([])
})

test('dont warn when v-slot used on root level <template> inside custom component', () => {
const onError = vi.fn()

transformWithCodegen(
`<template>
<div is="vue:customComp"><template v-slot="slotProps"></template></div>
</template>`,
{ onError },
)

expect(onError.mock.calls).toEqual([])
})
})
})
41 changes: 41 additions & 0 deletions packages/compiler-core/__tests__/transforms/vSlot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { createObjectMatcher } from '../testUtils'
import { PatchFlags } from '@vue/shared'
import { transformFor } from '../../src/transforms/vFor'
import { transformIf } from '../../src/transforms/vIf'
import type { CompilerError } from '../../src/errors'

function parseWithSlots(template: string, options: CompilerOptions = {}) {
const ast = parse(template, {
Expand Down Expand Up @@ -928,6 +929,46 @@ describe('compiler: transform component slots', () => {
},
})
})

test('error when v-slot used on non-root level <template>', () => {
const onError = vi.fn()

const { root } = parseWithSlots(
`<Bar><template><template #header> Header </template></template></Bar>`,
{ onError },
)

expect(onError.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_V_SLOT_TEMPLATE_NOT_ROOT,
loc: (root.children[0] as any).children[0].children[0].loc,
},
])
})

test('error when v-slot used on non-root level <template> with v-if', () => {
const onError = vi.fn().mockImplementation((error: CompilerError) => {
throw error
})

expect(() => {
parseWithSlots(
`<Bar><template v-if="true"><template #header> Header </template></template></Bar>`,
{ onError },
)
}).toThrow()

expect(onError.mock.calls[0]).toMatchObject([
{
code: ErrorCodes.X_V_SLOT_TEMPLATE_NOT_ROOT,
loc: {
start: { column: 28, line: 1, offset: 27 },
end: { column: 65, line: 1, offset: 64 },
source: '<template #header> Header </template>',
},
},
])
})
})

describe(`with whitespace: 'preserve'`, () => {
Expand Down
8 changes: 4 additions & 4 deletions packages/compiler-core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ export enum ErrorCodes {
X_V_SLOT_DUPLICATE_SLOT_NAMES,
X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
X_V_SLOT_MISPLACED,
X_V_SLOT_TEMPLATE_NOT_ROOT,
X_V_MODEL_NO_EXPRESSION,
X_V_MODEL_MALFORMED_EXPRESSION,
X_V_MODEL_ON_SCOPE_VARIABLE,
X_V_MODEL_ON_PROPS,
X_INVALID_EXPRESSION,
X_KEEP_ALIVE_INVALID_CHILDREN,
X_TEMPLATE_NOT_ROOT,

// generic errors
X_PREFIX_ID_NOT_SUPPORTED,
Expand Down Expand Up @@ -152,9 +152,6 @@ 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 All @@ -176,6 +173,9 @@ export const errorMessages: Record<ErrorCodes, string> = {
`Extraneous children found when component already has explicitly named ` +
`default slot. These children will be ignored.`,
[ErrorCodes.X_V_SLOT_MISPLACED]: `v-slot can only be used on components or <template> tags.`,
[ErrorCodes.X_V_SLOT_TEMPLATE_NOT_ROOT]:
`<template v-slot> can only appear at the root level inside ` +
`the receiving component`,
[ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
[ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE]: `v-model cannot be used on v-for or v-slot scope variables because they are not writable.`,
Expand Down
16 changes: 1 addition & 15 deletions packages/compiler-core/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,7 @@ import {
isArray,
isString,
} from '@vue/shared'
import {
ErrorCodes,
createCompilerError,
defaultOnError,
defaultOnWarn,
} from './errors'
import { defaultOnError, defaultOnWarn } from './errors'
import {
CREATE_COMMENT,
FRAGMENT,
Expand Down Expand Up @@ -501,15 +496,6 @@ export function createStructuralDirectiveTransform(
// structural directive transforms are not concerned with slots
// as they are handled separately in vSlot.ts
if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
if (
context.parent &&
'tagType' in context.parent &&
context.parent.tagType !== ElementTypes.COMPONENT
) {
context.onError(
createCompilerError(ErrorCodes.X_TEMPLATE_NOT_ROOT, node.loc),
)
}
return
}
const exitFns = []
Expand Down
13 changes: 13 additions & 0 deletions packages/compiler-core/src/transforms/vSlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ export const trackSlotScopes: NodeTransform = (node, context) => {
(node.tagType === ElementTypes.COMPONENT ||
node.tagType === ElementTypes.TEMPLATE)
) {
const { parent } = context
if (
node.tagType === ElementTypes.TEMPLATE &&
parent &&
(('tagType' in parent && parent.tagType !== ElementTypes.COMPONENT) ||
(parent.type && parent.type > NodeTypes.ELEMENT)) &&
node.props.some(isVSlot)
) {
context.onError(
createCompilerError(ErrorCodes.X_V_SLOT_TEMPLATE_NOT_ROOT, node.loc),
)
}

// We are only checking non-empty v-slot here
// since we only care about slots that introduce scope variables.
const vSlot = findDir(node, 'slot')
Expand Down

0 comments on commit 27ed27e

Please sign in to comment.