Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

types(slots): Add typed slots #2693

Closed
wants to merge 12 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,71 @@ return { a, props, emit }
}"
`;

exports[`SFC compile <script setup> > defineProps/defineSlots in multi-variable declaration (full removal) 1`] = `
"export default {
props: ['item'],
slots: ['a'],
setup(__props, { expose, slots }) {
expose();

const props = __props;



return { props, slots }
}

}"
`;

exports[`SFC compile <script setup> > defineProps/defineSlots in multi-variable declaration 1`] = `
"export default {
props: ['item'],
slots: ['a'],
setup(__props, { expose, slots }) {
expose();

const props = __props;

const a = 1;

return { props, a, slots }
}

}"
`;

exports[`SFC compile <script setup> > defineProps/defineSlots in multi-variable declaration fix #6757 1`] = `
"export default {
props: ['item'],
slots: ['a'],
setup(__props, { expose, slots }) {
expose();

const props = __props;

const a = 1;

return { a, props, slots }
}

}"
`;

exports[`SFC compile <script setup> > defineSlots() 1`] = `
"export default {
slots: ['foo', 'bar'],
setup(__props, { expose, slots: mySlots }) {
expose();



return { mySlots }
}

}"
`;

exports[`SFC compile <script setup> > dev mode import usage check > TS annotations 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
import { Foo, Bar, Baz, Qux, Fred } from './x'
Expand Down
62 changes: 60 additions & 2 deletions packages/compiler-sfc/__tests__/compileScript.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,65 @@ const myEmit = defineEmits(['foo', 'bar'])
expect(content).toMatch(`emits: ['a'],`)
})

test('defineSlots()', () => {
const { content, bindings } = compile(`
<script setup>
const mySlots = defineSlots(['foo', 'bar'])
</script>
`)
assertCode(content)
expect(bindings).toStrictEqual({
mySlots: BindingTypes.SETUP_CONST
})
// should remove defineOptions import and call
expect(content).not.toMatch('defineSlots')
// should generate correct setup signature
expect(content).toMatch(`setup(__props, { expose, slots: mySlots }) {`)
// should include context options in default export
expect(content).toMatch(`export default {
slots: ['foo', 'bar'],`)
})

test('defineProps/defineSlots in multi-variable declaration', () => {
const { content } = compile(`
<script setup>
const props = defineProps(['item']),
a = 1,
slots = defineSlots(['a']);
</script>
`)
assertCode(content)
expect(content).toMatch(`const a = 1;`) // test correct removal
expect(content).toMatch(`props: ['item'],`)
expect(content).toMatch(`slots: ['a'],`)
})

test('defineProps/defineSlots in multi-variable declaration fix #6757 ', () => {
const { content } = compile(`
<script setup>
const a = 1,
props = defineProps(['item']),
slots = defineSlots(['a']);
</script>
`)
assertCode(content)
expect(content).toMatch(`const a = 1;`) // test correct removal
expect(content).toMatch(`props: ['item'],`)
expect(content).toMatch(`slots: ['a'],`)
})

test('defineProps/defineSlots in multi-variable declaration (full removal)', () => {
const { content } = compile(`
<script setup>
const props = defineProps(['item']),
slots = defineSlots(['a']);
</script>
`)
assertCode(content)
expect(content).toMatch(`props: ['item'],`)
expect(content).toMatch(`slots: ['a'],`)
})

test('defineExpose()', () => {
const { content } = compile(`
<script setup>
Expand Down Expand Up @@ -1136,7 +1195,7 @@ const emit = defineEmits(['a', 'b'])
`)
assertCode(content)
})

// #7111
test('withDefaults (static) w/ production mode', () => {
const { content } = compile(
Expand Down Expand Up @@ -1277,7 +1336,6 @@ const emit = defineEmits(['a', 'b'])
expect(content).toMatch(`emits: ["foo", "bar"]`)
})


test('defineEmits w/ type from normal script', () => {
const { content } = compile(`
<script lang="ts">
Expand Down
111 changes: 108 additions & 3 deletions packages/compiler-sfc/src/compileScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { shouldTransform, transformAST } from '@vue/reactivity-transform'
// Special compiler macros
const DEFINE_PROPS = 'defineProps'
const DEFINE_EMITS = 'defineEmits'
const DEFINE_SLOTS = 'defineSlots'
const DEFINE_EXPOSE = 'defineExpose'
const WITH_DEFAULTS = 'withDefaults'

Expand Down Expand Up @@ -141,6 +142,9 @@ type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
type EmitsDeclType = FromNormalScript<
TSFunctionType | TSTypeLiteral | TSInterfaceBody
>
type SlotsDeclType = FromNormalScript<
TSFunctionType | TSTypeLiteral | TSInterfaceBody
>

/**
* Compile `<script setup>`
Expand Down Expand Up @@ -286,6 +290,7 @@ export function compileScript(
let defaultExport: Node | undefined
let hasDefinePropsCall = false
let hasDefineEmitCall = false
let hasDefineSlotCall = false
let hasDefineExposeCall = false
let hasDefaultExportName = false
let hasDefaultExportRender = false
Expand All @@ -300,11 +305,16 @@ export function compileScript(
let emitsTypeDecl: EmitsDeclType | undefined
let emitsTypeDeclRaw: Node | undefined
let emitIdentifier: string | undefined
let slotsRuntimeDecl: Node | undefined
let slotsTypeDecl: EmitsDeclType | undefined
let slotsTypeDeclRaw: Node | undefined
let slotsIdentifier: string | undefined
let hasAwait = false
let hasInlinedSsrRenderFn = false
// props/emits declared via types
const typeDeclaredProps: Record<string, PropTypeData> = {}
const typeDeclaredEmits: Set<string> = new Set()
const typeDeclaredSlots: Set<string> = new Set()
// record declared types for runtime props type generation
const declaredTypes: Record<string, string[]> = {}
// props destructure data
Expand Down Expand Up @@ -590,6 +600,48 @@ export function compileScript(
return true
}

function processDefineSlots(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_SLOTS)) {
return false
}
if (hasDefineSlotCall) {
error(`duplicate ${DEFINE_SLOTS}() call`, node)
}
hasDefineSlotCall = true
slotsRuntimeDecl = node.arguments[0]
if (node.typeParameters) {
if (slotsRuntimeDecl) {
error(
`${DEFINE_SLOTS}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`,
node
)
}

slotsTypeDeclRaw = node.typeParameters.params[0]
slotsTypeDecl = resolveQualifiedType(
slotsTypeDeclRaw,
node => node.type === 'TSFunctionType' || node.type === 'TSTypeLiteral'
) as SlotsDeclType | undefined

if (!slotsTypeDecl) {
error(
`type argument passed to ${DEFINE_SLOTS}() must be a function type, ` +
`a literal type with call signatures, or a reference to the above types.`,
slotsTypeDeclRaw
)
}
}

if (declId) {
slotsIdentifier =
declId.type === 'Identifier'
? declId.name
: scriptSetup!.content.slice(declId.start!, declId.end!)
}

return true
}
function getAstBody(): Statement[] {
return scriptAst
? [...scriptSetupAst.body, ...scriptAst.body]
Expand Down Expand Up @@ -1194,6 +1246,7 @@ export function compileScript(
if (
processDefineProps(node.expression) ||
processDefineEmits(node.expression) ||
processDefineSlots(node.expression) ||
processWithDefaults(node.expression)
) {
s.remove(node.start! + startOffset, node.end! + startOffset)
Expand All @@ -1219,7 +1272,8 @@ export function compileScript(
processDefineProps(decl.init, decl.id, node.kind) ||
processWithDefaults(decl.init, decl.id, node.kind)
const isDefineEmits = processDefineEmits(decl.init, decl.id)
if (isDefineProps || isDefineEmits) {
const isDefineSlots = processDefineSlots(decl.init, decl.id)
if (isDefineProps || isDefineEmits || isDefineSlots) {
if (left === 1) {
s.remove(node.start! + startOffset, node.end! + startOffset)
} else {
Expand Down Expand Up @@ -1344,20 +1398,24 @@ export function compileScript(
}
}

// 4. extract runtime props/emits code from setup context type
// 4. extract runtime props/emits/slots code from setup context type
if (propsTypeDecl) {
extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes, isProd)
}
if (emitsTypeDecl) {
extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
}
if (slotsTypeDecl) {
extractRuntimeSlots(slotsTypeDecl, typeDeclaredSlots)
}

// 5. check useOptions args to make sure it doesn't reference setup scope
// variables
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
checkInvalidScopeReference(propsDestructureDecl, DEFINE_PROPS)
checkInvalidScopeReference(emitsRuntimeDecl, DEFINE_EMITS)
checkInvalidScopeReference(slotsRuntimeDecl, DEFINE_SLOTS)

// 6. remove non-script content
if (script) {
Expand Down Expand Up @@ -1483,6 +1541,11 @@ export function compileScript(
emitIdentifier === `emit` ? `emit` : `emit: ${emitIdentifier}`
)
}
if (slotsIdentifier) {
destructureElements.push(
slotsIdentifier === `slots` ? `slots` : `slots: ${slotsIdentifier}`
)
}
if (destructureElements.length) {
args += `, { ${destructureElements.join(', ')} }`
if (emitsTypeDecl) {
Expand All @@ -1494,6 +1557,17 @@ export function compileScript(
emitsTypeDecl.end!
)}), expose: any, slots: any, attrs: any }`
}

// TODO review this part
if (slotsTypeDecl) {
const content = slotsTypeDecl.__fromNormalScript
? script!.content
: scriptSetup.content
args += `: { slots: (${content.slice(
slotsTypeDecl.start!,
slotsTypeDecl.end!
)}), expose: any, emit: any, attrs: any }`
}
Comment on lines +1561 to +1570
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this, this seems that might cause an issue when we have both slot and emit

}

// 10. generate return statement
Expand Down Expand Up @@ -1644,7 +1718,13 @@ export function compileScript(
} else if (emitsTypeDecl) {
runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
}

if (slotsRuntimeDecl) {
runtimeOptions += `\n slots: ${scriptSetup.content
.slice(slotsRuntimeDecl.start!, slotsRuntimeDecl.end!)
.trim()},`
} else if (slotsTypeDecl) {
runtimeOptions += genRuntimeSlots(typeDeclaredSlots)
}
// <script setup> components are closed by default. If the user did not
// explicitly call `defineExpose`, call expose() with no args.
const exposeCall =
Expand Down Expand Up @@ -2015,6 +2095,23 @@ function extractRuntimeEmits(
}
}

function extractRuntimeSlots(
node: TSFunctionType | TSTypeLiteral | TSInterfaceBody,
slots: Set<string>
) {
if (node.type === 'TSTypeLiteral' || node.type === 'TSInterfaceBody') {
const members = node.type === 'TSTypeLiteral' ? node.members : node.body
for (let t of members) {
if (t.type === 'TSCallSignatureDeclaration') {
extractEventNames(t.parameters[0], slots)
}
}
return
} else {
extractEventNames(node.parameters[0], slots)
}
}

function extractEventNames(
eventName: Identifier | RestElement,
emits: Set<string>
Expand Down Expand Up @@ -2054,6 +2151,14 @@ function genRuntimeEmits(emits: Set<string>) {
: ``
}

function genRuntimeSlots(slots: Set<string>) {
return slots.size
? `\n slots: [${Array.from(slots)
.map(p => JSON.stringify(p))
.join(', ')}],`
: ``
}

function isCallOf(
node: Node | null | undefined,
test: string | ((id: string) => boolean) | null | undefined
Expand Down