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 }
|
return { emit }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,13 +47,13 @@ const emit = defineEmits(['a', 'b'])
|
||||||
|
|
||||||
test('w/ type (union)', () => {
|
test('w/ type (union)', () => {
|
||||||
const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)`
|
const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)`
|
||||||
expect(() =>
|
const { content } = compile(`
|
||||||
compile(`
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const emit = defineEmits<${type}>()
|
const emit = defineEmits<${type}>()
|
||||||
</script>
|
</script>
|
||||||
`)
|
`)
|
||||||
).toThrow()
|
assertCode(content)
|
||||||
|
expect(content).toMatch(`emits: ["foo", "bar", "baz"]`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('w/ type (type literal w/ call signatures)', () => {
|
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)
|
const elements = resolveTypeElements(ctx, node)
|
||||||
for (const key in elements) {
|
for (const key in elements) {
|
||||||
const e = elements[key]
|
const e = elements[key]
|
||||||
let type: string[] | undefined
|
let type = inferRuntimeType(ctx, e)
|
||||||
let skipCheck = false
|
let skipCheck = false
|
||||||
if (e.type === 'TSMethodSignature') {
|
// skip check for result containing unknown types
|
||||||
type = ['Function']
|
if (type.includes(UNKNOWN_TYPE)) {
|
||||||
} else if (e.typeAnnotation) {
|
if (type.includes('Boolean') || type.includes('Function')) {
|
||||||
type = inferRuntimeType(ctx, e.typeAnnotation.typeAnnotation)
|
type = type.filter(t => t !== UNKNOWN_TYPE)
|
||||||
// skip check for result containing unknown types
|
skipCheck = true
|
||||||
if (type.includes(UNKNOWN_TYPE)) {
|
} else {
|
||||||
if (type.includes('Boolean') || type.includes('Function')) {
|
type = ['null']
|
||||||
type = type.filter(t => t !== UNKNOWN_TYPE)
|
|
||||||
skipCheck = true
|
|
||||||
} else {
|
|
||||||
type = ['null']
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
props.push({
|
props.push({
|
||||||
|
|
|
@ -16,7 +16,8 @@ import { UNKNOWN_TYPE } from './utils'
|
||||||
import { ScriptCompileContext } from './context'
|
import { ScriptCompileContext } from './context'
|
||||||
import { ImportBinding } from '../compileScript'
|
import { ImportBinding } from '../compileScript'
|
||||||
import { TSInterfaceDeclaration } from '@babel/types'
|
import { TSInterfaceDeclaration } from '@babel/types'
|
||||||
import { hasOwn } from '@vue/shared'
|
import { hasOwn, isArray } from '@vue/shared'
|
||||||
|
import { Expression } from '@babel/types'
|
||||||
|
|
||||||
export interface TypeScope {
|
export interface TypeScope {
|
||||||
filename: string
|
filename: string
|
||||||
|
@ -63,24 +64,37 @@ function innerResolveTypeElements(
|
||||||
addCallSignature(ret, node)
|
addCallSignature(ret, node)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
case 'TSExpressionWithTypeArguments':
|
case 'TSExpressionWithTypeArguments': // referenced by interface extends
|
||||||
case 'TSTypeReference':
|
case 'TSTypeReference':
|
||||||
return resolveTypeElements(ctx, resolveTypeReference(ctx, node))
|
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)
|
ctx.error(`Unsupported type in SFC macro: ${node.type}`, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCallSignature(
|
function addCallSignature(
|
||||||
elements: ResolvedElements,
|
elements: ResolvedElements,
|
||||||
node: TSCallSignatureDeclaration | TSFunctionType
|
node:
|
||||||
|
| TSCallSignatureDeclaration
|
||||||
|
| TSFunctionType
|
||||||
|
| (TSCallSignatureDeclaration | TSFunctionType)[]
|
||||||
) {
|
) {
|
||||||
if (!elements.__callSignatures) {
|
if (!elements.__callSignatures) {
|
||||||
Object.defineProperty(elements, '__callSignatures', {
|
Object.defineProperty(elements, '__callSignatures', {
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
value: [node]
|
value: isArray(node) ? node : [node]
|
||||||
})
|
})
|
||||||
} else {
|
} 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
|
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(
|
function resolveInterfaceMembers(
|
||||||
ctx: ScriptCompileContext,
|
ctx: ScriptCompileContext,
|
||||||
node: TSInterfaceDeclaration
|
node: TSInterfaceDeclaration
|
||||||
|
@ -252,6 +305,11 @@ export function inferRuntimeType(
|
||||||
}
|
}
|
||||||
return types.size ? Array.from(types) : ['Object']
|
return types.size ? Array.from(types) : ['Object']
|
||||||
}
|
}
|
||||||
|
case 'TSPropertySignature':
|
||||||
|
if (node.typeAnnotation) {
|
||||||
|
return inferRuntimeType(ctx, node.typeAnnotation.typeAnnotation)
|
||||||
|
}
|
||||||
|
case 'TSMethodSignature':
|
||||||
case 'TSFunctionType':
|
case 'TSFunctionType':
|
||||||
return ['Function']
|
return ['Function']
|
||||||
case 'TSArrayType':
|
case 'TSArrayType':
|
||||||
|
|
Loading…
Reference in New Issue