Skip to content

Commit

Permalink
feat(better-define): support union types
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed Jan 29, 2023
1 parent 7003c92 commit 37f4fd3
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .changeset/tough-singers-remain.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 types
4 changes: 2 additions & 2 deletions packages/api/src/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type {
Identifier,
ImportNamespaceSpecifier,
ImportSpecifier,
Node,
Statement,
TSCallSignatureDeclaration,
TSConstructSignatureDeclaration,
Expand Down Expand Up @@ -155,7 +154,8 @@ export async function resolveTSProperties({
}
return properties
} else {
throw new Error(`unknown node: ${(type as Node)?.type}`)
// @ts-expect-error type is never
throw new Error(`unknown node: ${type?.type}`)
}

function filterValidExtends(
Expand Down
93 changes: 92 additions & 1 deletion packages/api/src/vue/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
TSPropertySignature,
TSType,
TSTypeLiteral,
TSUnionType,
VariableDeclaration,
} from '@babel/types'

Expand Down Expand Up @@ -253,6 +254,96 @@ export async function handleTSPropsDefinition({
throw new SyntaxError(`Cannot resolve TS definition.`)

const { type: definitionsAst, scope } = resolved
if (definitionsAst.type === 'TSUnionType') {
const unionDefs: TSProps['definitions'][] = []
const keys = new Set<string>()
for (const type of definitionsAst.types) {
const defs = await resolveDefinitions({ type, scope }).then(
({ definitions }) => definitions
)
Object.keys(defs).map((key) => keys.add(key))
unionDefs.push(defs)
}

const results: TSProps['definitions'] = {}
for (const key of keys) {
let optional = false
let result: TSPropsMethod | TSPropsProperty | undefined

for (const defMap of unionDefs) {
const def = defMap[key]
if (!def) {
optional = true
continue
}
optional ||= def.optional

if (!result) {
result = def
continue
}

if (result.type === 'method' && def.type === 'method') {
result.methods.push(...def.methods)
} else if (result.type === 'property' && def.type === 'property') {
if (!def.value) {
continue
} else if (!result.value) {
result = def
continue
}

if (
def.value.ast.type === 'TSImportType' ||
def.value.ast.type === 'TSDeclareFunction' ||
def.value.ast.type === 'TSEnumDeclaration' ||
def.value.ast.type === 'TSInterfaceDeclaration' ||
def.value.ast.type === 'TSModuleDeclaration' ||
result.value.ast.type === 'TSImportType' ||
result.value.ast.type === 'TSDeclareFunction' ||
result.value.ast.type === 'TSEnumDeclaration' ||
result.value.ast.type === 'TSInterfaceDeclaration' ||
result.value.ast.type === 'TSModuleDeclaration'
) {
// no way!
continue
}

if (result.value.ast.type === 'TSUnionType') {
result.value.ast.types.push(def.value.ast)
} else {
// overwrite original to union type
result = {
type: 'property',
value: buildDefinition({
scope,
type: {
type: 'TSUnionType',
types: [result.value.ast, def.value.ast],
} satisfies TSUnionType,
}),
signature: null as any,
optional,
addByAPI: false,
}
}
} else {
throw new SyntaxError(
`Cannot resolve TS definition. Union type contains different types of results.`
)
}
}

if (result) {
results[key] = { ...result, optional }
}
}

return {
definitions: results,
definitionsAst: buildDefinition({ scope, type: definitionsAst }),
}
}
if (
definitionsAst.type !== 'TSInterfaceDeclaration' &&
definitionsAst.type !== 'TSTypeLiteral' &&
Expand Down Expand Up @@ -409,7 +500,7 @@ export interface TSProps extends PropsBase {

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

/**
Expand Down
62 changes: 62 additions & 0 deletions packages/better-define/tests/__snapshots__/fixtures.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -525,3 +525,65 @@ var specialKey = /* @__PURE__ */ _export_sfc(_sfc_main, [__FILE__]);
export { specialKey as default };
"
`;

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

exports[`fixtures > tests/fixtures/union.vue > isProduction = true 1`] = `
"import { defineComponent } from 'vue';
var _sfc_main = /* @__PURE__ */ defineComponent({
__name: \\"union\\",
props: {
\\"type\\": null,
\\"foo\\": null,
\\"optional\\": { type: Boolean },
\\"bar\\": null
},
setup(__props) {
return () => {
};
}
});
var _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
var union = /* @__PURE__ */ _export_sfc(_sfc_main, [__FILE__]);
export { union as default };
"
`;
6 changes: 6 additions & 0 deletions packages/better-define/tests/fixtures/union.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script setup lang="ts">
defineProps<
| { type: 'foo'; foo: string; optional?: boolean }
| { type: 'bar'; bar: number; optional?: boolean }
>()
</script>

0 comments on commit 37f4fd3

Please sign in to comment.