Skip to content

Commit dd28b1e

Browse files
wh1teAltersxzz
andauthored
feat(resolve-type): support infer generics (#766)
Co-authored-by: 三咲智子 Kevin Deng <[email protected]>
1 parent 653f0a8 commit dd28b1e

File tree

3 files changed

+226
-12
lines changed

3 files changed

+226
-12
lines changed

packages/babel-plugin-resolve-type/src/index.ts

Lines changed: 112 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,17 @@ const plugin: (
8484
node.arguments.push(options);
8585
}
8686

87-
node.arguments[1] = processProps(comp, options) || options;
88-
node.arguments[1] = processEmits(comp, node.arguments[1]) || options;
87+
let propsGenerics: BabelCore.types.TSType | undefined;
88+
let emitsGenerics: BabelCore.types.TSType | undefined;
89+
if (node.typeParameters && node.typeParameters.params.length > 0) {
90+
propsGenerics = node.typeParameters.params[0];
91+
emitsGenerics = node.typeParameters.params[1];
92+
}
93+
94+
node.arguments[1] =
95+
processProps(comp, propsGenerics, options) || options;
96+
node.arguments[1] =
97+
processEmits(comp, emitsGenerics, node.arguments[1]) || options;
8998
},
9099
VariableDeclarator(path) {
91100
inferComponentName(path);
@@ -125,6 +134,7 @@ const plugin: (
125134

126135
function processProps(
127136
comp: BabelCore.types.Function,
137+
generics: BabelCore.types.TSType | undefined,
128138
options:
129139
| BabelCore.types.ArgumentPlaceholder
130140
| BabelCore.types.SpreadElement
@@ -134,10 +144,18 @@ const plugin: (
134144
if (!props) return;
135145

136146
if (props.type === 'AssignmentPattern') {
137-
ctx!.propsTypeDecl = getTypeAnnotation(props.left);
147+
if (generics) {
148+
ctx!.propsTypeDecl = resolveTypeReference(generics);
149+
} else {
150+
ctx!.propsTypeDecl = getTypeAnnotation(props.left);
151+
}
138152
ctx!.propsRuntimeDefaults = props.right;
139153
} else {
140-
ctx!.propsTypeDecl = getTypeAnnotation(props);
154+
if (generics) {
155+
ctx!.propsTypeDecl = resolveTypeReference(generics);
156+
} else {
157+
ctx!.propsTypeDecl = getTypeAnnotation(props);
158+
}
141159
}
142160

143161
if (!ctx!.propsTypeDecl) return;
@@ -157,20 +175,26 @@ const plugin: (
157175

158176
function processEmits(
159177
comp: BabelCore.types.Function,
178+
generics: BabelCore.types.TSType | undefined,
160179
options:
161180
| BabelCore.types.ArgumentPlaceholder
162181
| BabelCore.types.SpreadElement
163182
| BabelCore.types.Expression
164183
) {
184+
let emitType: BabelCore.types.Node | undefined;
185+
if (generics) {
186+
emitType = resolveTypeReference(generics);
187+
}
188+
165189
const setupCtx = comp.params[1] && getTypeAnnotation(comp.params[1]);
166190
if (
167-
!setupCtx ||
168-
!t.isTSTypeReference(setupCtx) ||
169-
!t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' })
170-
)
171-
return;
172-
173-
const emitType = setupCtx.typeParameters?.params[0];
191+
!emitType &&
192+
setupCtx &&
193+
t.isTSTypeReference(setupCtx) &&
194+
t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' })
195+
) {
196+
emitType = setupCtx.typeParameters?.params[0];
197+
}
174198
if (!emitType) return;
175199

176200
ctx!.emitsTypeDecl = emitType;
@@ -185,8 +209,84 @@ const plugin: (
185209
t.objectProperty(t.identifier('emits'), ast)
186210
);
187211
}
188-
});
189212

213+
function resolveTypeReference(typeNode: BabelCore.types.TSType) {
214+
if (!ctx) return;
215+
216+
if (t.isTSTypeReference(typeNode)) {
217+
const typeName = getTypeReferenceName(typeNode);
218+
if (typeName) {
219+
const typeDeclaration = findTypeDeclaration(typeName);
220+
if (typeDeclaration) {
221+
return typeDeclaration;
222+
}
223+
}
224+
}
225+
226+
return;
227+
}
228+
229+
function getTypeReferenceName(typeRef: BabelCore.types.TSTypeReference) {
230+
if (t.isIdentifier(typeRef.typeName)) {
231+
return typeRef.typeName.name;
232+
} else if (t.isTSQualifiedName(typeRef.typeName)) {
233+
const parts: string[] = [];
234+
let current: BabelCore.types.TSEntityName = typeRef.typeName;
235+
236+
while (t.isTSQualifiedName(current)) {
237+
if (t.isIdentifier(current.right)) {
238+
parts.unshift(current.right.name);
239+
}
240+
current = current.left;
241+
}
242+
243+
if (t.isIdentifier(current)) {
244+
parts.unshift(current.name);
245+
}
246+
247+
return parts.join('.');
248+
}
249+
return null;
250+
}
251+
252+
function findTypeDeclaration(typeName: string) {
253+
if (!ctx) return null;
254+
255+
for (const statement of ctx.ast) {
256+
if (
257+
t.isTSInterfaceDeclaration(statement) &&
258+
statement.id.name === typeName
259+
) {
260+
return t.tsTypeLiteral(statement.body.body);
261+
}
262+
263+
if (
264+
t.isTSTypeAliasDeclaration(statement) &&
265+
statement.id.name === typeName
266+
) {
267+
return statement.typeAnnotation;
268+
}
269+
270+
if (t.isExportNamedDeclaration(statement) && statement.declaration) {
271+
if (
272+
t.isTSInterfaceDeclaration(statement.declaration) &&
273+
statement.declaration.id.name === typeName
274+
) {
275+
return t.tsTypeLiteral(statement.declaration.body.body);
276+
}
277+
278+
if (
279+
t.isTSTypeAliasDeclaration(statement.declaration) &&
280+
statement.declaration.id.name === typeName
281+
) {
282+
return statement.declaration.typeAnnotation;
283+
}
284+
}
285+
}
286+
287+
return null;
288+
}
289+
});
190290
export default plugin;
191291

192292
function getTypeAnnotation(node: BabelCore.types.Node) {

packages/babel-plugin-resolve-type/test/__snapshots__/resolve-type.test.tsx.snap

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,22 @@ defineComponent((props, {
8383
});"
8484
`;
8585

86+
exports[`resolve type > runtime emits > with generic emit type 1`] = `
87+
"import { type SetupContext, defineComponent } from 'vue';
88+
type EmitEvents = {
89+
change(val: string): void;
90+
click(): void;
91+
};
92+
defineComponent<{}, EmitEvents>((props, {
93+
emit
94+
}) => {
95+
emit('change');
96+
return () => {};
97+
}, {
98+
emits: ["change", "click"]
99+
});"
100+
`;
101+
86102
exports[`resolve type > runtime props > basic 1`] = `
87103
"import { defineComponent, h } from 'vue';
88104
interface Props {
@@ -129,6 +145,28 @@ defineComponent((props: {
129145
});"
130146
`;
131147
148+
exports[`resolve type > runtime props > with generic 1`] = `
149+
"import { defineComponent, h } from 'vue';
150+
interface Props {
151+
msg: string;
152+
optional?: boolean;
153+
}
154+
defineComponent<Props>(props => {
155+
return () => h('div', props.msg);
156+
}, {
157+
props: {
158+
msg: {
159+
type: String,
160+
required: true
161+
},
162+
optional: {
163+
type: Boolean,
164+
required: false
165+
}
166+
}
167+
});"
168+
`;
169+
132170
exports[`resolve type > runtime props > with static default value 1`] = `
133171
"import { defineComponent, h } from 'vue';
134172
defineComponent((props: {
@@ -148,6 +186,31 @@ defineComponent((props: {
148186
});"
149187
`;
150188
189+
exports[`resolve type > runtime props > with static default value and generic 1`] = `
190+
"import { defineComponent, h } from 'vue';
191+
type Props = {
192+
msg: string;
193+
optional?: boolean;
194+
};
195+
defineComponent<Props>((props = {
196+
msg: 'hello'
197+
}) => {
198+
return () => h('div', props.msg);
199+
}, {
200+
props: {
201+
msg: {
202+
type: String,
203+
required: true,
204+
default: 'hello'
205+
},
206+
optional: {
207+
type: Boolean,
208+
required: false
209+
}
210+
}
211+
});"
212+
`;
213+
151214
exports[`resolve type > w/ tsx 1`] = `
152215
"import { type SetupContext, defineComponent } from 'vue';
153216
defineComponent(() => {

packages/babel-plugin-resolve-type/test/resolve-type.test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,38 @@ describe('resolve type', () => {
3131
expect(result).toMatchSnapshot();
3232
});
3333

34+
test('with generic', async () => {
35+
const result = await transform(
36+
`
37+
import { defineComponent, h } from 'vue';
38+
interface Props {
39+
msg: string;
40+
optional?: boolean;
41+
}
42+
defineComponent<Props>((props) => {
43+
return () => h('div', props.msg);
44+
})
45+
`
46+
);
47+
expect(result).toMatchSnapshot();
48+
});
49+
50+
test('with static default value and generic', async () => {
51+
const result = await transform(
52+
`
53+
import { defineComponent, h } from 'vue';
54+
type Props = {
55+
msg: string;
56+
optional?: boolean;
57+
};
58+
defineComponent<Props>((props = { msg: 'hello' }) => {
59+
return () => h('div', props.msg);
60+
})
61+
`
62+
);
63+
expect(result).toMatchSnapshot();
64+
});
65+
3466
test('with static default value', async () => {
3567
const result = await transform(
3668
`
@@ -75,6 +107,25 @@ describe('resolve type', () => {
75107
);
76108
expect(result).toMatchSnapshot();
77109
});
110+
111+
test('with generic emit type', async () => {
112+
const result = await transform(
113+
`
114+
import { type SetupContext, defineComponent } from 'vue';
115+
type EmitEvents = {
116+
change(val: string): void;
117+
click(): void;
118+
};
119+
defineComponent<{}, EmitEvents>(
120+
(props, { emit }) => {
121+
emit('change');
122+
return () => {};
123+
}
124+
);
125+
`
126+
);
127+
expect(result).toMatchSnapshot();
128+
});
78129
});
79130

80131
test('w/ tsx', async () => {

0 commit comments

Comments
 (0)