feat(resolve-type): support infer generics (#766)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
white
2025-08-10 09:17:51 +08:00
committed by GitHub
parent 653f0a843a
commit dd28b1e752
3 changed files with 226 additions and 12 deletions

View File

@ -84,8 +84,17 @@ const plugin: (
node.arguments.push(options); node.arguments.push(options);
} }
node.arguments[1] = processProps(comp, options) || options; let propsGenerics: BabelCore.types.TSType | undefined;
node.arguments[1] = processEmits(comp, node.arguments[1]) || options; 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) { VariableDeclarator(path) {
inferComponentName(path); inferComponentName(path);
@ -125,6 +134,7 @@ const plugin: (
function processProps( function processProps(
comp: BabelCore.types.Function, comp: BabelCore.types.Function,
generics: BabelCore.types.TSType | undefined,
options: options:
| BabelCore.types.ArgumentPlaceholder | BabelCore.types.ArgumentPlaceholder
| BabelCore.types.SpreadElement | BabelCore.types.SpreadElement
@ -134,10 +144,18 @@ const plugin: (
if (!props) return; if (!props) return;
if (props.type === 'AssignmentPattern') { 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; ctx!.propsRuntimeDefaults = props.right;
} else { } else {
ctx!.propsTypeDecl = getTypeAnnotation(props); if (generics) {
ctx!.propsTypeDecl = resolveTypeReference(generics);
} else {
ctx!.propsTypeDecl = getTypeAnnotation(props);
}
} }
if (!ctx!.propsTypeDecl) return; if (!ctx!.propsTypeDecl) return;
@ -157,20 +175,26 @@ const plugin: (
function processEmits( function processEmits(
comp: BabelCore.types.Function, comp: BabelCore.types.Function,
generics: BabelCore.types.TSType | undefined,
options: options:
| BabelCore.types.ArgumentPlaceholder | BabelCore.types.ArgumentPlaceholder
| BabelCore.types.SpreadElement | BabelCore.types.SpreadElement
| BabelCore.types.Expression | BabelCore.types.Expression
) { ) {
let emitType: BabelCore.types.Node | undefined;
if (generics) {
emitType = resolveTypeReference(generics);
}
const setupCtx = comp.params[1] && getTypeAnnotation(comp.params[1]); const setupCtx = comp.params[1] && getTypeAnnotation(comp.params[1]);
if ( if (
!setupCtx || !emitType &&
!t.isTSTypeReference(setupCtx) || setupCtx &&
!t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' }) t.isTSTypeReference(setupCtx) &&
) t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' })
return; ) {
emitType = setupCtx.typeParameters?.params[0];
const emitType = setupCtx.typeParameters?.params[0]; }
if (!emitType) return; if (!emitType) return;
ctx!.emitsTypeDecl = emitType; ctx!.emitsTypeDecl = emitType;
@ -185,8 +209,84 @@ const plugin: (
t.objectProperty(t.identifier('emits'), ast) 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;
}
});
export default plugin; export default plugin;
function getTypeAnnotation(node: BabelCore.types.Node) { function getTypeAnnotation(node: BabelCore.types.Node) {

View File

@ -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`] = ` exports[`resolve type > runtime props > basic 1`] = `
"import { defineComponent, h } from 'vue'; "import { defineComponent, h } from 'vue';
interface Props { interface Props {
@ -129,6 +145,28 @@ defineComponent((props: {
});" });"
`; `;
exports[`resolve type > runtime props > with generic 1`] = `
"import { defineComponent, h } from 'vue';
interface Props {
msg: string;
optional?: boolean;
}
defineComponent<Props>(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`] = ` exports[`resolve type > runtime props > with static default value 1`] = `
"import { defineComponent, h } from 'vue'; "import { defineComponent, h } from 'vue';
defineComponent((props: { defineComponent((props: {
@ -148,6 +186,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>((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`] = ` exports[`resolve type > w/ tsx 1`] = `
"import { type SetupContext, defineComponent } from 'vue'; "import { type SetupContext, defineComponent } from 'vue';
defineComponent(() => { defineComponent(() => {

View File

@ -31,6 +31,38 @@ describe('resolve type', () => {
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
test('with generic', async () => {
const result = await transform(
`
import { defineComponent, h } from 'vue';
interface Props {
msg: string;
optional?: boolean;
}
defineComponent<Props>((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>((props = { msg: 'hello' }) => {
return () => h('div', props.msg);
})
`
);
expect(result).toMatchSnapshot();
});
test('with static default value', async () => { test('with static default value', async () => {
const result = await transform( const result = await transform(
` `
@ -75,6 +107,25 @@ describe('resolve type', () => {
); );
expect(result).toMatchSnapshot(); 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 () => { test('w/ tsx', async () => {