Skip to content

Commit

Permalink
feat(api): support TSMappedType
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed Mar 25, 2023
1 parent d944bb0 commit 9335bfd
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 52 deletions.
6 changes: 6 additions & 0 deletions .changeset/hot-bugs-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@vue-macros/better-define': minor
'@vue-macros/api': minor
---

support union key (TSMappedType)
146 changes: 98 additions & 48 deletions packages/api/src/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import path from 'node:path'
import {
babelParse,
getFileCodeAndLang,
isStaticExpression,
resolveLiteral,
resolveObjectKey,
} from '@vue-macros/common'
import { isDeclaration } from '@babel/types'
Expand All @@ -21,6 +23,7 @@ import type {
TSInterfaceBody,
TSInterfaceDeclaration,
TSIntersectionType,
TSMappedType,
TSMethodSignature,
TSModuleBlock,
TSModuleDeclaration,
Expand All @@ -30,6 +33,7 @@ import type {
TSTypeAliasDeclaration,
TSTypeElement,
TSTypeLiteral,
UnaryExpression,
} from '@babel/types'

export type TSDeclaration =
Expand Down Expand Up @@ -57,7 +61,7 @@ export interface TSProperties {
{
value: TSResolvedType<TSType> | null
optional: boolean
signature: TSResolvedType<TSPropertySignature>
signature: TSResolvedType<TSPropertySignature | TSMappedType>
}
>
}
Expand Down Expand Up @@ -100,62 +104,108 @@ export async function resolveTSProperties({
type,
scope,
}: TSResolvedType<
TSInterfaceDeclaration | TSInterfaceBody | TSTypeLiteral | TSIntersectionType
| TSInterfaceDeclaration
| TSInterfaceBody
| TSTypeLiteral
| TSIntersectionType
| TSMappedType
>): Promise<TSProperties> {
if (type.type === 'TSInterfaceBody') {
return resolveTypeElements(scope, type.body)
} else if (type.type === 'TSTypeLiteral') {
return resolveTypeElements(scope, type.members)
} else if (type.type === 'TSInterfaceDeclaration') {
let properties = resolveTypeElements(scope, type.body.body)
if (type.extends) {
const resolvedExtends = (
await Promise.all(
type.extends.map((node) =>
node.expression.type === 'Identifier'
? resolveTSReferencedType({
scope,
type: node.expression,
})
: undefined
)
)
)
// eslint-disable-next-line unicorn/no-array-callback-reference
.filter(filterValidExtends)

if (resolvedExtends.length > 0) {
const ext = (
switch (type.type) {
case 'TSInterfaceBody':
return resolveTypeElements(scope, type.body)
case 'TSTypeLiteral':
return resolveTypeElements(scope, type.members)
case 'TSInterfaceDeclaration': {
let properties = resolveTypeElements(scope, type.body.body)
if (type.extends) {
const resolvedExtends = (
await Promise.all(
resolvedExtends.map((resolved) => resolveTSProperties(resolved))
type.extends.map((node) =>
node.expression.type === 'Identifier'
? resolveTSReferencedType({
scope,
type: node.expression,
})
: undefined
)
)
).reduceRight((acc, curr) => mergeTSProperties(acc, curr))
properties = mergeTSProperties(ext, properties)
)
// eslint-disable-next-line unicorn/no-array-callback-reference
.filter(filterValidExtends)

if (resolvedExtends.length > 0) {
const ext = (
await Promise.all(
resolvedExtends.map((resolved) => resolveTSProperties(resolved))
)
).reduceRight((acc, curr) => mergeTSProperties(acc, curr))
properties = mergeTSProperties(ext, properties)
}
}
return properties
}
return properties
} else if (type.type === 'TSIntersectionType') {
let properties: TSProperties = {
callSignatures: [],
constructSignatures: [],
methods: {},
properties: {},
case 'TSIntersectionType': {
let properties: TSProperties = {
callSignatures: [],
constructSignatures: [],
methods: {},
properties: {},
}
for (const subType of type.types) {
const resolved = await resolveTSReferencedType({
scope,
type: subType,
})
if (!filterValidExtends(resolved)) continue
properties = mergeTSProperties(
properties,
await resolveTSProperties(resolved)
)
}
return properties
}
for (const subType of type.types) {
const resolved = await resolveTSReferencedType({
case 'TSMappedType': {
const properties: TSProperties = {
callSignatures: [],
constructSignatures: [],
methods: {},
properties: {},
}
if (!type.typeParameter.constraint) return properties

const constraint = await resolveTSReferencedType({
type: type.typeParameter.constraint,
scope,
type: subType,
})
if (!filterValidExtends(resolved)) continue
properties = mergeTSProperties(
properties,
await resolveTSProperties(resolved)
)
if (!constraint?.type) return properties

const types =
constraint.type.type === 'TSUnionType'
? constraint.type.types
: [constraint.type]

for (const subType of types) {
if (subType.type !== 'TSLiteralType') continue
const literal = subType.literal
if (!isStaticExpression(literal)) continue
const key = resolveLiteral(
literal as Exclude<typeof literal, UnaryExpression>
)
if (!key) continue
properties.properties[String(key)] = {
value: type.typeAnnotation
? { scope, type: type.typeAnnotation }
: null,
optional: type.optional === '+' || type.optional === true,
signature: { type, scope },
}
}

return properties
}
return properties
} else {
// @ts-expect-error type is never
throw new Error(`unknown node: ${type?.type}`)
default:
// @ts-expect-error type is never
throw new Error(`unknown node: ${type?.type}`)
}

function filterValidExtends(
Expand Down
16 changes: 12 additions & 4 deletions packages/api/src/vue/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
StringLiteral,
TSInterfaceDeclaration,
TSIntersectionType,
TSMappedType,
TSMethodSignature,
TSPropertySignature,
TSType,
Expand Down Expand Up @@ -368,9 +369,12 @@ export async function handleTSPropsDefinition({
}
} else if (
definitionsAst.type !== 'TSInterfaceDeclaration' &&
definitionsAst.type !== 'TSTypeLiteral'
definitionsAst.type !== 'TSTypeLiteral' &&
definitionsAst.type !== 'TSMappedType'
)
throw new SyntaxError(`Cannot resolve TS definition.`)
throw new SyntaxError(
`Cannot resolve TS definition: ${definitionsAst.type}.`
)

const properties = await resolveTSProperties({
scope,
Expand Down Expand Up @@ -504,7 +508,7 @@ export interface TSPropsProperty {
type: 'property'
value: ASTDefinition<TSResolvedType['type']> | undefined
optional: boolean
signature: ASTDefinition<TSPropertySignature>
signature: ASTDefinition<TSPropertySignature | TSMappedType>

/** Whether added by `addProp` API */
addByAPI: boolean
Expand All @@ -521,7 +525,11 @@ export interface TSProps extends PropsBase {

definitions: Record<string | number, TSPropsMethod | TSPropsProperty>
definitionsAst: ASTDefinition<
TSInterfaceDeclaration | TSTypeLiteral | TSIntersectionType | TSUnionType
| TSInterfaceDeclaration
| TSTypeLiteral
| TSIntersectionType
| TSUnionType
| TSMappedType
>

/**
Expand Down
46 changes: 46 additions & 0 deletions packages/better-define/tests/__snapshots__/fixtures.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,52 @@ export { unionEmits as default };
"
`;

exports[`fixtures > tests/fixtures/union-props.vue > isProduction = false 1`] = `
"import { defineComponent } from 'vue';
import _export_sfc from '/plugin-vue/export-helper';
var _sfc_main = /* @__PURE__ */ defineComponent({
__name: \\"union-props\\",
props: {
\\"foo\\": { type: [String, Number], required: true },
\\"bar\\": { type: [String, Number], required: true },
\\"optional\\": { type: Boolean, required: false }
},
setup(__props) {
return () => {
};
}
});
var unionProps = /* @__PURE__ */ _export_sfc(_sfc_main, [__FILE__]);
export { unionProps as default };
"
`;

exports[`fixtures > tests/fixtures/union-props.vue > isProduction = true 1`] = `
"import { defineComponent } from 'vue';
import _export_sfc from '/plugin-vue/export-helper';
var _sfc_main = /* @__PURE__ */ defineComponent({
__name: \\"union-props\\",
props: {
\\"foo\\": null,
\\"bar\\": null,
\\"optional\\": { type: Boolean }
},
setup(__props) {
return () => {
};
}
});
var unionProps = /* @__PURE__ */ _export_sfc(_sfc_main, [__FILE__]);
export { unionProps as default };
"
`;

exports[`fixtures > tests/fixtures/unresolved.vue > isProduction = false 1`] = `
"import { defineComponent } from 'vue';
import _export_sfc from '/plugin-vue/export-helper';
Expand Down
8 changes: 8 additions & 0 deletions packages/better-define/tests/fixtures/union-props.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script setup lang="ts">
type T = 'foo' | 'bar'
defineProps<
{ [K in T]: string | number } & {
[K in 'optional']?: boolean
}
>()
</script>

0 comments on commit 9335bfd

Please sign in to comment.