Skip to content

Commit

Permalink
feat(compiler-sfc): support intersection and union types in macros
Browse files Browse the repository at this point in the history
close #7553
  • Loading branch information
yyx990803 committed Apr 12, 2023
1 parent a6dedc3 commit d1f973b
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)', () => {
Expand Down
179 changes: 179 additions & 0 deletions packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
21 changes: 8 additions & 13 deletions packages/compiler-sfc/src/script/defineProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
68 changes: 63 additions & 5 deletions packages/compiler-sfc/src/script/resolveType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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':
Expand Down

0 comments on commit d1f973b

Please sign in to comment.