mirror of https://github.com/vuejs/core.git
feat(compiler-sfc): support intersection and union types in macros
close #7553
This commit is contained in:
parent
a6dedc33ba
commit
d1f973bff8
|
@ -191,6 +191,22 @@ export default /*#__PURE__*/_defineComponent({
|
|||
|
||||
|
||||
|
||||
return { emit }
|
||||
}
|
||||
|
||||
})"
|
||||
`;
|
||||
|
||||
exports[`defineEmits > w/ type (union) 1`] = `
|
||||
"import { defineComponent as _defineComponent } from 'vue'
|
||||
|
||||
export default /*#__PURE__*/_defineComponent({
|
||||
emits: [\\"foo\\", \\"bar\\", \\"baz\\"],
|
||||
setup(__props, { expose: __expose, emit }) {
|
||||
__expose();
|
||||
|
||||
|
||||
|
||||
return { emit }
|
||||
}
|
||||
|
||||
|
|
|
@ -47,13 +47,13 @@ const emit = defineEmits(['a', 'b'])
|
|||
|
||||
test('w/ type (union)', () => {
|
||||
const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)`
|
||||
expect(() =>
|
||||
compile(`
|
||||
const { content } = compile(`
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<${type}>()
|
||||
</script>
|
||||
`)
|
||||
).toThrow()
|
||||
assertCode(content)
|
||||
expect(content).toMatch(`emits: ["foo", "bar", "baz"]`)
|
||||
})
|
||||
|
||||
test('w/ type (type literal w/ call signatures)', () => {
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
import { TSTypeAliasDeclaration } from '@babel/types'
|
||||
import { parse } from '../../src'
|
||||
import { ScriptCompileContext } from '../../src/script/context'
|
||||
import {
|
||||
inferRuntimeType,
|
||||
resolveTypeElements
|
||||
} from '../../src/script/resolveType'
|
||||
|
||||
describe('resolveType', () => {
|
||||
test('type literal', () => {
|
||||
const { elements, callSignatures } = resolve(`type Target = {
|
||||
foo: number // property
|
||||
bar(): void // method
|
||||
'baz': string // string literal key
|
||||
(e: 'foo'): void // call signature
|
||||
(e: 'bar'): void
|
||||
}`)
|
||||
expect(elements).toStrictEqual({
|
||||
foo: ['Number'],
|
||||
bar: ['Function'],
|
||||
baz: ['String']
|
||||
})
|
||||
expect(callSignatures?.length).toBe(2)
|
||||
})
|
||||
|
||||
test('reference type', () => {
|
||||
expect(
|
||||
resolve(`
|
||||
type Aliased = { foo: number }
|
||||
type Target = Aliased
|
||||
`).elements
|
||||
).toStrictEqual({
|
||||
foo: ['Number']
|
||||
})
|
||||
})
|
||||
|
||||
test('reference exported type', () => {
|
||||
expect(
|
||||
resolve(`
|
||||
export type Aliased = { foo: number }
|
||||
type Target = Aliased
|
||||
`).elements
|
||||
).toStrictEqual({
|
||||
foo: ['Number']
|
||||
})
|
||||
})
|
||||
|
||||
test('reference interface', () => {
|
||||
expect(
|
||||
resolve(`
|
||||
interface Aliased { foo: number }
|
||||
type Target = Aliased
|
||||
`).elements
|
||||
).toStrictEqual({
|
||||
foo: ['Number']
|
||||
})
|
||||
})
|
||||
|
||||
test('reference exported interface', () => {
|
||||
expect(
|
||||
resolve(`
|
||||
export interface Aliased { foo: number }
|
||||
type Target = Aliased
|
||||
`).elements
|
||||
).toStrictEqual({
|
||||
foo: ['Number']
|
||||
})
|
||||
})
|
||||
|
||||
test('reference interface extends', () => {
|
||||
expect(
|
||||
resolve(`
|
||||
export interface A { a(): void }
|
||||
export interface B extends A { b: boolean }
|
||||
interface C { c: string }
|
||||
interface Aliased extends B, C { foo: number }
|
||||
type Target = Aliased
|
||||
`).elements
|
||||
).toStrictEqual({
|
||||
a: ['Function'],
|
||||
b: ['Boolean'],
|
||||
c: ['String'],
|
||||
foo: ['Number']
|
||||
})
|
||||
})
|
||||
|
||||
test('function type', () => {
|
||||
expect(
|
||||
resolve(`
|
||||
type Target = (e: 'foo') => void
|
||||
`).callSignatures?.length
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
test('reference function type', () => {
|
||||
expect(
|
||||
resolve(`
|
||||
type Fn = (e: 'foo') => void
|
||||
type Target = Fn
|
||||
`).callSignatures?.length
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
test('intersection type', () => {
|
||||
expect(
|
||||
resolve(`
|
||||
type Foo = { foo: number }
|
||||
type Bar = { bar: string }
|
||||
type Baz = { bar: string | boolean }
|
||||
type Target = { self: any } & Foo & Bar & Baz
|
||||
`).elements
|
||||
).toStrictEqual({
|
||||
self: ['Unknown'],
|
||||
foo: ['Number'],
|
||||
// both Bar & Baz has 'bar', but Baz['bar] is wider so it should be
|
||||
// preferred
|
||||
bar: ['String', 'Boolean']
|
||||
})
|
||||
})
|
||||
|
||||
// #7553
|
||||
test('union type', () => {
|
||||
expect(
|
||||
resolve(`
|
||||
interface CommonProps {
|
||||
size?: 'xl' | 'l' | 'm' | 's' | 'xs'
|
||||
}
|
||||
|
||||
type ConditionalProps =
|
||||
| {
|
||||
color: 'normal' | 'primary' | 'secondary'
|
||||
appearance: 'normal' | 'outline' | 'text'
|
||||
}
|
||||
| {
|
||||
color: number
|
||||
appearance: 'outline'
|
||||
note: string
|
||||
}
|
||||
|
||||
type Target = CommonProps & ConditionalProps
|
||||
`).elements
|
||||
).toStrictEqual({
|
||||
size: ['String'],
|
||||
color: ['String', 'Number'],
|
||||
appearance: ['String'],
|
||||
note: ['String']
|
||||
})
|
||||
})
|
||||
|
||||
// describe('built-in utility types', () => {
|
||||
|
||||
// })
|
||||
|
||||
describe('errors', () => {
|
||||
test('error on computed keys', () => {
|
||||
expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow(
|
||||
`computed keys are not supported in types referenced by SFC macros`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function resolve(code: string) {
|
||||
const { descriptor } = parse(`<script setup lang="ts">${code}</script>`)
|
||||
const ctx = new ScriptCompileContext(descriptor, { id: 'test' })
|
||||
const targetDecl = ctx.scriptSetupAst!.body.find(
|
||||
s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target'
|
||||
) as TSTypeAliasDeclaration
|
||||
const raw = resolveTypeElements(ctx, targetDecl.typeAnnotation)
|
||||
const elements: Record<string, string[]> = {}
|
||||
for (const key in raw) {
|
||||
elements[key] = inferRuntimeType(ctx, raw[key])
|
||||
}
|
||||
return {
|
||||
elements,
|
||||
callSignatures: raw.__callSignatures,
|
||||
raw
|
||||
}
|
||||
}
|
|
@ -193,20 +193,15 @@ function resolveRuntimePropsFromType(
|
|||
const elements = resolveTypeElements(ctx, node)
|
||||
for (const key in elements) {
|
||||
const e = elements[key]
|
||||
let type: string[] | undefined
|
||||
let type = inferRuntimeType(ctx, e)
|
||||
let skipCheck = false
|
||||
if (e.type === 'TSMethodSignature') {
|
||||
type = ['Function']
|
||||
} else if (e.typeAnnotation) {
|
||||
type = inferRuntimeType(ctx, e.typeAnnotation.typeAnnotation)
|
||||
// skip check for result containing unknown types
|
||||
if (type.includes(UNKNOWN_TYPE)) {
|
||||
if (type.includes('Boolean') || type.includes('Function')) {
|
||||
type = type.filter(t => t !== UNKNOWN_TYPE)
|
||||
skipCheck = true
|
||||
} else {
|
||||
type = ['null']
|
||||
}
|
||||
// skip check for result containing unknown types
|
||||
if (type.includes(UNKNOWN_TYPE)) {
|
||||
if (type.includes('Boolean') || type.includes('Function')) {
|
||||
type = type.filter(t => t !== UNKNOWN_TYPE)
|
||||
skipCheck = true
|
||||
} else {
|
||||
type = ['null']
|
||||
}
|
||||
}
|
||||
props.push({
|
||||
|
|
|
@ -16,7 +16,8 @@ import { UNKNOWN_TYPE } from './utils'
|
|||
import { ScriptCompileContext } from './context'
|
||||
import { ImportBinding } from '../compileScript'
|
||||
import { TSInterfaceDeclaration } from '@babel/types'
|
||||
import { hasOwn } from '@vue/shared'
|
||||
import { hasOwn, isArray } from '@vue/shared'
|
||||
import { Expression } from '@babel/types'
|
||||
|
||||
export interface TypeScope {
|
||||
filename: string
|
||||
|
@ -63,24 +64,37 @@ function innerResolveTypeElements(
|
|||
addCallSignature(ret, node)
|
||||
return ret
|
||||
}
|
||||
case 'TSExpressionWithTypeArguments':
|
||||
case 'TSExpressionWithTypeArguments': // referenced by interface extends
|
||||
case 'TSTypeReference':
|
||||
return resolveTypeElements(ctx, resolveTypeReference(ctx, node))
|
||||
case 'TSUnionType':
|
||||
case 'TSIntersectionType':
|
||||
return mergeElements(
|
||||
node.types.map(t => resolveTypeElements(ctx, t)),
|
||||
node.type
|
||||
)
|
||||
}
|
||||
ctx.error(`Unsupported type in SFC macro: ${node.type}`, node)
|
||||
}
|
||||
|
||||
function addCallSignature(
|
||||
elements: ResolvedElements,
|
||||
node: TSCallSignatureDeclaration | TSFunctionType
|
||||
node:
|
||||
| TSCallSignatureDeclaration
|
||||
| TSFunctionType
|
||||
| (TSCallSignatureDeclaration | TSFunctionType)[]
|
||||
) {
|
||||
if (!elements.__callSignatures) {
|
||||
Object.defineProperty(elements, '__callSignatures', {
|
||||
enumerable: false,
|
||||
value: [node]
|
||||
value: isArray(node) ? node : [node]
|
||||
})
|
||||
} else {
|
||||
elements.__callSignatures.push(node)
|
||||
if (isArray(node)) {
|
||||
elements.__callSignatures.push(...node)
|
||||
} else {
|
||||
elements.__callSignatures.push(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,6 +126,45 @@ function typeElementsToMap(
|
|||
return ret
|
||||
}
|
||||
|
||||
function mergeElements(
|
||||
maps: ResolvedElements[],
|
||||
type: 'TSUnionType' | 'TSIntersectionType'
|
||||
): ResolvedElements {
|
||||
const res: ResolvedElements = Object.create(null)
|
||||
for (const m of maps) {
|
||||
for (const key in m) {
|
||||
if (!(key in res)) {
|
||||
res[key] = m[key]
|
||||
} else {
|
||||
res[key] = createProperty(res[key].key, type, [res[key], m[key]])
|
||||
}
|
||||
}
|
||||
if (m.__callSignatures) {
|
||||
addCallSignature(res, m.__callSignatures)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
function createProperty(
|
||||
key: Expression,
|
||||
type: 'TSUnionType' | 'TSIntersectionType',
|
||||
types: Node[]
|
||||
): TSPropertySignature {
|
||||
return {
|
||||
type: 'TSPropertySignature',
|
||||
key,
|
||||
kind: 'get',
|
||||
typeAnnotation: {
|
||||
type: 'TSTypeAnnotation',
|
||||
typeAnnotation: {
|
||||
type,
|
||||
types: types as TSType[]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveInterfaceMembers(
|
||||
ctx: ScriptCompileContext,
|
||||
node: TSInterfaceDeclaration
|
||||
|
@ -252,6 +305,11 @@ export function inferRuntimeType(
|
|||
}
|
||||
return types.size ? Array.from(types) : ['Object']
|
||||
}
|
||||
case 'TSPropertySignature':
|
||||
if (node.typeAnnotation) {
|
||||
return inferRuntimeType(ctx, node.typeAnnotation.typeAnnotation)
|
||||
}
|
||||
case 'TSMethodSignature':
|
||||
case 'TSFunctionType':
|
||||
return ['Function']
|
||||
case 'TSArrayType':
|
||||
|
|
Loading…
Reference in New Issue