diff --git a/packages/babel-plugin-resolve-type/src/index.ts b/packages/babel-plugin-resolve-type/src/index.ts index 854597ad..5d5fc966 100644 --- a/packages/babel-plugin-resolve-type/src/index.ts +++ b/packages/babel-plugin-resolve-type/src/index.ts @@ -1,4 +1,7 @@ +import { codeFrameColumns } from '@babel/code-frame'; import type * as BabelCore from '@babel/core'; +import { addNamed } from '@babel/helper-module-imports'; +import { declare } from '@babel/helper-plugin-utils'; import { parseExpression } from '@babel/parser'; import { type SimpleTypeResolveContext, @@ -6,9 +9,6 @@ import { extractRuntimeEmits, extractRuntimeProps, } from '@vue/compiler-sfc'; -import { codeFrameColumns } from '@babel/code-frame'; -import { addNamed } from '@babel/helper-module-imports'; -import { declare } from '@babel/helper-plugin-utils'; export { SimpleTypeResolveOptions as Options }; @@ -79,8 +79,17 @@ export default declare(({ types: t }, options) => { node.arguments.push(options); } - node.arguments[1] = processProps(comp, options) || options; - node.arguments[1] = processEmits(comp, node.arguments[1]) || options; + let propsGenerics: BabelCore.types.TSType | undefined; + let emitsGenerics: BabelCore.types.TSType | undefined; + if (node.typeParameters && node.typeParameters.params.length > 0) { + propsGenerics = node.typeParameters.params[0]; + emitsGenerics = node.typeParameters.params[1]; + } + + node.arguments[1] = + processProps(comp, propsGenerics, options) || options; + node.arguments[1] = + processEmits(comp, emitsGenerics, node.arguments[1]) || options; }, VariableDeclarator(path) { inferComponentName(path); @@ -118,6 +127,7 @@ export default declare(({ types: t }, options) => { function processProps( comp: BabelCore.types.Function, + generics: BabelCore.types.TSType | undefined, options: | BabelCore.types.ArgumentPlaceholder | BabelCore.types.SpreadElement @@ -127,10 +137,18 @@ export default declare(({ types: t }, options) => { if (!props) return; if (props.type === 'AssignmentPattern') { - ctx!.propsTypeDecl = getTypeAnnotation(props.left); + if (generics) { + ctx!.propsTypeDecl = resolveTypeReference(generics); + } else { + ctx!.propsTypeDecl = getTypeAnnotation(props.left); + } ctx!.propsRuntimeDefaults = props.right; } else { - ctx!.propsTypeDecl = getTypeAnnotation(props); + if (generics) { + ctx!.propsTypeDecl = resolveTypeReference(generics); + } else { + ctx!.propsTypeDecl = getTypeAnnotation(props); + } } if (!ctx!.propsTypeDecl) return; @@ -150,20 +168,26 @@ export default declare(({ types: t }, options) => { function processEmits( comp: BabelCore.types.Function, + generics: BabelCore.types.TSType | undefined, options: | BabelCore.types.ArgumentPlaceholder | BabelCore.types.SpreadElement | BabelCore.types.Expression ) { + let emitType: BabelCore.types.Node | undefined; + if (generics) { + emitType = resolveTypeReference(generics); + } + const setupCtx = comp.params[1] && getTypeAnnotation(comp.params[1]); if ( - !setupCtx || - !t.isTSTypeReference(setupCtx) || - !t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' }) - ) - return; - - const emitType = setupCtx.typeParameters?.params[0]; + !emitType && + setupCtx && + t.isTSTypeReference(setupCtx) && + t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' }) + ) { + emitType = setupCtx.typeParameters?.params[0]; + } if (!emitType) return; ctx!.emitsTypeDecl = emitType; @@ -178,6 +202,83 @@ export default declare(({ types: t }, options) => { t.objectProperty(t.identifier('emits'), ast) ); } + + function resolveTypeReference(typeNode: BabelCore.types.TSType) { + if (!ctx) return; + + if (t.isTSTypeReference(typeNode)) { + const typeName = getTypeReferenceName(typeNode); + if (typeName) { + const typeDeclaration = findTypeDeclaration(typeName); + if (typeDeclaration) { + return typeDeclaration; + } + } + } + + return; + } + + function getTypeReferenceName(typeRef: BabelCore.types.TSTypeReference) { + if (t.isIdentifier(typeRef.typeName)) { + return typeRef.typeName.name; + } else if (t.isTSQualifiedName(typeRef.typeName)) { + const parts: string[] = []; + let current: BabelCore.types.TSEntityName = typeRef.typeName; + + while (t.isTSQualifiedName(current)) { + if (t.isIdentifier(current.right)) { + parts.unshift(current.right.name); + } + current = current.left; + } + + if (t.isIdentifier(current)) { + parts.unshift(current.name); + } + + return parts.join('.'); + } + return null; + } + + function findTypeDeclaration(typeName: string) { + if (!ctx) return null; + + for (const statement of ctx.ast) { + if ( + t.isTSInterfaceDeclaration(statement) && + statement.id.name === typeName + ) { + return t.tsTypeLiteral(statement.body.body); + } + + if ( + t.isTSTypeAliasDeclaration(statement) && + statement.id.name === typeName + ) { + return statement.typeAnnotation; + } + + if (t.isExportNamedDeclaration(statement) && statement.declaration) { + if ( + t.isTSInterfaceDeclaration(statement.declaration) && + statement.declaration.id.name === typeName + ) { + return t.tsTypeLiteral(statement.declaration.body.body); + } + + if ( + t.isTSTypeAliasDeclaration(statement.declaration) && + statement.declaration.id.name === typeName + ) { + return statement.declaration.typeAnnotation; + } + } + } + + return null; + } }); function getTypeAnnotation(node: BabelCore.types.Node) { diff --git a/packages/babel-plugin-resolve-type/test/__snapshots__/resolve-type.test.tsx.snap b/packages/babel-plugin-resolve-type/test/__snapshots__/resolve-type.test.tsx.snap index 3d94e68e..ff03e4ae 100644 --- a/packages/babel-plugin-resolve-type/test/__snapshots__/resolve-type.test.tsx.snap +++ b/packages/babel-plugin-resolve-type/test/__snapshots__/resolve-type.test.tsx.snap @@ -83,6 +83,22 @@ defineComponent((props, { });" `; +exports[`resolve type > runtime emits > with generic emit type 1`] = ` +"import { type SetupContext, defineComponent } from 'vue'; +type EmitEvents = { + change(val: string): void; + click(): void; +}; +defineComponent<{}, EmitEvents>((props, { + emit +}) => { + emit('change'); + return () => {}; +}, { + emits: ["change", "click"] +});" +`; + exports[`resolve type > runtime props > basic 1`] = ` "import { defineComponent, h } from 'vue'; interface Props { @@ -129,6 +145,50 @@ defineComponent((props: { });" `; +exports[`resolve type > runtime props > with generic 1`] = ` +"import { defineComponent, h } from 'vue'; +interface Props { + msg: string; + optional?: boolean; +} +defineComponent(props => { + return () => h('div', props.msg); +}, { + props: { + msg: { + type: String, + required: true + }, + optional: { + type: Boolean, + required: false + } + } +});" +`; + +exports[`resolve type > runtime props > with generic type parameter 1`] = ` +"import { defineComponent, h } from 'vue'; +interface Props { + msg: string; + optional?: boolean; +} +defineComponent(props => { + return () => h('div', props.msg); +}, { + props: { + msg: { + type: String, + required: true + }, + optional: { + type: Boolean, + required: false + } + } +});" +`; + exports[`resolve type > runtime props > with static default value 1`] = ` "import { defineComponent, h } from 'vue'; defineComponent((props: { @@ -148,6 +208,31 @@ defineComponent((props: { });" `; +exports[`resolve type > runtime props > with static default value and generic 1`] = ` +"import { defineComponent, h } from 'vue'; +type Props = { + msg: string; + optional?: boolean; +}; +defineComponent((props = { + msg: 'hello' +}) => { + return () => h('div', props.msg); +}, { + props: { + msg: { + type: String, + required: true, + default: 'hello' + }, + optional: { + type: Boolean, + required: false + } + } +});" +`; + exports[`resolve type > w/ tsx 1`] = ` "import { type SetupContext, defineComponent } from 'vue'; defineComponent(() => { diff --git a/packages/babel-plugin-resolve-type/test/resolve-type.test.tsx b/packages/babel-plugin-resolve-type/test/resolve-type.test.tsx index 43700225..466d4132 100644 --- a/packages/babel-plugin-resolve-type/test/resolve-type.test.tsx +++ b/packages/babel-plugin-resolve-type/test/resolve-type.test.tsx @@ -31,6 +31,38 @@ describe('resolve type', () => { expect(result).toMatchSnapshot(); }); + test('with generic', async () => { + const result = await transform( + ` + import { defineComponent, h } from 'vue'; + interface Props { + msg: string; + optional?: boolean; + } + defineComponent((props) => { + return () => h('div', props.msg); + }) + ` + ); + expect(result).toMatchSnapshot(); + }); + + test('with static default value and generic', async () => { + const result = await transform( + ` + import { defineComponent, h } from 'vue'; + type Props = { + msg: string; + optional?: boolean; + }; + defineComponent((props = { msg: 'hello' }) => { + return () => h('div', props.msg); + }) + ` + ); + expect(result).toMatchSnapshot(); + }); + test('with static default value', async () => { const result = await transform( ` @@ -75,6 +107,25 @@ describe('resolve type', () => { ); expect(result).toMatchSnapshot(); }); + + test('with generic emit type', async () => { + const result = await transform( + ` + import { type SetupContext, defineComponent } from 'vue'; + type EmitEvents = { + change(val: string): void; + click(): void; + }; + defineComponent<{}, EmitEvents>( + (props, { emit }) => { + emit('change'); + return () => {}; + } + ); + ` + ); + expect(result).toMatchSnapshot(); + }); }); test('w/ tsx', async () => {