Skip to content
This repository was archived by the owner on Nov 27, 2023. It is now read-only.

Commit 40bccb0

Browse files
renrizzoloKnisterPeter
authored andcommitted
feat: create intersection types for components with dot notation component members
1 parent 38603a0 commit 40bccb0

9 files changed

+372
-88
lines changed

src/typings.ts

+216-73
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,12 @@ export function createTypings(
7373
}
7474
const alreadyDefined: string[] = [];
7575

76+
const componentDots = getComponentDotProperties(ast, componentNames);
7677
componentNames.forEach((componentName) => {
7778
const exportType = getComponentExportType(ast, componentName);
7879
const propTypes = getPropTypes(ast, componentName);
79-
if (exportType) {
80+
const intersection = getIntersection(componentDots, componentName);
81+
if (exportType || componentDots.length) {
8082
alreadyDefined.push(componentName);
8183
createExportedTypes(
8284
m,
@@ -86,6 +88,7 @@ export function createTypings(
8688
propTypes,
8789
importedPropTypes,
8890
exportType,
91+
intersection,
8992
options
9093
);
9194
}
@@ -94,49 +97,56 @@ export function createTypings(
9497
// top level object variables
9598
const componentObject = getComponentNamesByObject(ast, componentNames);
9699

97-
componentObject.forEach(({ name, properties = {} }) => {
100+
componentObject.forEach(({ name, properties }) => {
98101
const obj = dom.create.objectType([]);
99102
let hasType;
100103

101104
Object.keys(properties).forEach((k) => {
102105
const { key, value } = properties[k];
103-
componentNames.forEach((componentName) => {
104-
// if a property matches an existing component
105-
// add it to the object definition
106-
if (value.type === 'Identifier' && value.name === componentName) {
107-
const exportType = getComponentExportType(ast, componentName);
108-
const propTypes = getPropTypes(ast, value.name);
109-
// if it was exported individually, it will already have been typed earlier
110-
if (!alreadyDefined.includes(componentName)) {
111-
createExportedTypes(
112-
m,
113-
ast,
114-
value.name,
115-
reactComponentName,
116-
propTypes,
117-
importedPropTypes,
118-
exportType,
119-
options
120-
);
121-
}
122-
123-
if (propTypes) {
124-
hasType = true;
125-
const type1 = dom.create.namedTypeReference(value.name);
126-
const typeBase = dom.create.typeof(type1);
127-
const b = dom.create.property(key.name, typeBase);
128-
obj.members.push(b);
129-
}
106+
// if a property matches an existing component
107+
// add it to the object definition
108+
if (value.type === 'Identifier' && componentNames.includes(value.name)) {
109+
const exportType =
110+
name === '_default'
111+
? undefined
112+
: getComponentExportType(ast, value.name);
113+
const propTypes = getPropTypes(ast, value.name);
114+
const intersection = getIntersection(componentDots, name);
115+
116+
// if it was exported individually, it will already have been typed earlier
117+
if (!alreadyDefined.includes(value.name)) {
118+
createExportedTypes(
119+
m,
120+
ast,
121+
value.name,
122+
reactComponentName,
123+
propTypes,
124+
importedPropTypes,
125+
exportType,
126+
intersection,
127+
options
128+
);
130129
}
131-
});
130+
131+
if (propTypes) {
132+
hasType = true;
133+
const type1 = dom.create.namedTypeReference(value.name);
134+
const typeBase = dom.create.typeof(type1);
135+
const b = dom.create.property(key.name, typeBase);
136+
obj.members.push(b);
137+
}
138+
}
132139
});
133140
if (hasType) {
134141
const exportType = getComponentExportType(ast, name);
135142

136143
const objConst = dom.create.const(name, obj);
137144
m.members.push(objConst);
138145

139-
if (exportType === dom.DeclarationFlags.ExportDefault) {
146+
if (
147+
exportType === dom.DeclarationFlags.ExportDefault ||
148+
name === '_default'
149+
) {
140150
m.members.push(dom.create.exportDefault(name));
141151
} else {
142152
objConst.flags = exportType;
@@ -150,6 +160,24 @@ export function createTypings(
150160
return dom.emit(m, { tripleSlashDirectives });
151161
}
152162
}
163+
function getIntersection(
164+
componentDots: ComponentProperties[],
165+
componentName: string
166+
): string | null {
167+
const intersection = componentDots.find((v) => v.name === componentName);
168+
if (intersection) {
169+
const types = intersection.properties.map(
170+
(prop: ComponentProperties['properties'][0]) => {
171+
return `\t\t${prop.key}: typeof ${prop.value};`;
172+
}
173+
);
174+
175+
return ` & {
176+
${types.join('\n')}
177+
}`;
178+
}
179+
return null;
180+
}
153181

154182
function createExportedTypes(
155183
m: dom.ModuleDeclaration,
@@ -159,6 +187,7 @@ function createExportedTypes(
159187
propTypes: any,
160188
importedPropTypes: ImportedPropTypes,
161189
exportType: dom.DeclarationFlags | undefined,
190+
intersection: any,
162191
options: IOptions
163192
): void {
164193
const classComponent = isClassComponent(
@@ -177,32 +206,71 @@ function createExportedTypes(
177206
if (propTypes || classComponent) {
178207
m.members.push(interf);
179208
}
180-
181209
if (classComponent) {
182-
if (!exportType) {
183-
createClassComponent(m, componentName, reactComponentName, interf);
184-
} else {
185-
createExportedClassComponent(
186-
m,
187-
componentName,
188-
reactComponentName,
189-
exportType,
190-
interf
191-
);
192-
}
193-
} else if (!exportType) {
194-
createFunctionalComponent(m, componentName, propTypes, interf);
210+
createClassOrExportedClass(
211+
m,
212+
componentName,
213+
reactComponentName,
214+
exportType,
215+
interf
216+
);
195217
} else {
218+
createFunctionalOrExportedFunctionalComponent(
219+
m,
220+
componentName,
221+
propTypes,
222+
exportType!,
223+
intersection,
224+
interf
225+
);
226+
}
227+
}
228+
function createClassOrExportedClass(
229+
m: dom.ModuleDeclaration,
230+
componentName: string,
231+
reactComponentName: string | undefined,
232+
exportType: dom.DeclarationFlags | undefined,
233+
interf: dom.InterfaceDeclaration
234+
): void {
235+
if (exportType) {
236+
createExportedClassComponent(
237+
m,
238+
componentName,
239+
reactComponentName,
240+
exportType,
241+
interf
242+
);
243+
} else {
244+
createClassComponent(m, componentName, reactComponentName, interf);
245+
}
246+
}
247+
function createFunctionalOrExportedFunctionalComponent(
248+
m: dom.ModuleDeclaration,
249+
componentName: string,
250+
propTypes: any,
251+
exportType: dom.DeclarationFlags | undefined,
252+
intersection: any,
253+
interf: dom.InterfaceDeclaration
254+
): void {
255+
if (exportType) {
196256
createExportedFunctionalComponent(
197257
m,
198258
componentName,
199259
propTypes,
200260
exportType,
261+
intersection,
262+
interf
263+
);
264+
} else {
265+
createFunctionalComponent(
266+
m,
267+
componentName,
268+
propTypes,
269+
intersection,
201270
interf
202271
);
203272
}
204273
}
205-
206274
function createClassComponent(
207275
m: dom.ModuleDeclaration,
208276
componentName: string,
@@ -244,10 +312,13 @@ function createFunctionalComponent(
244312
m: dom.ModuleDeclaration,
245313
componentName: string,
246314
propTypes: any,
315+
intersection: any,
247316
interf: dom.InterfaceDeclaration
248317
): dom.ConstDeclaration {
249318
const typeDecl = dom.create.namedTypeReference(
250-
`React.FC${propTypes ? `<${interf.name}>` : ''}`
319+
`React.FC${propTypes ? `<${interf.name}>` : ''}${
320+
intersection ? intersection : ''
321+
}`
251322
);
252323
const constDecl = dom.create.const(componentName, typeDecl);
253324
m.members.push(constDecl);
@@ -260,12 +331,14 @@ function createExportedFunctionalComponent(
260331
componentName: string,
261332
propTypes: any,
262333
exportType: dom.DeclarationFlags,
334+
intersection: any,
263335
interf: dom.InterfaceDeclaration
264336
): void {
265337
const constDecl = createFunctionalComponent(
266338
m,
267339
componentName,
268340
propTypes,
341+
intersection,
269342
interf
270343
);
271344
if (exportType === dom.DeclarationFlags.ExportDefault) {
@@ -579,37 +652,107 @@ function getComponentNamesByJsxInBody(ast: AstQuery): string[] {
579652
}
580653
return [];
581654
}
655+
type ComponentProperties = {
656+
name: string;
657+
properties: {
658+
key: any;
659+
value: any;
660+
type?: any;
661+
}[];
662+
};
582663

583664
function getComponentNamesByObject(
584665
ast: AstQuery,
585666
componentNames: string[]
586-
): { name: string; properties: object | undefined }[] {
587-
const res = ast.query(`
588-
/:program *
589-
/ VariableDeclaration
590-
/ VariableDeclarator[
591-
/:init ObjectExpression
592-
// ObjectProperty
593-
],
594-
/:program *
595-
/ ExportNamedDeclaration
596-
// VariableDeclarator[
597-
/:init ObjectExpression
598-
// ObjectProperty
667+
): ComponentProperties[] {
668+
let arr: ComponentProperties[] = [];
669+
componentNames.forEach((name) => {
670+
const res = ast.query(`
671+
/:program *
672+
/ VariableDeclaration
673+
/ VariableDeclarator[
674+
/:init ObjectExpression
675+
// ObjectProperty
676+
/:value Identifier[@name == '${name}']
677+
],
678+
/:program *
679+
/ ExportNamedDeclaration
680+
// VariableDeclarator[
681+
/:init ObjectExpression
682+
// ObjectProperty
683+
/:value Identifier[@name == '${name}']
684+
],
685+
/:program *
686+
/ ExportDefaultDeclaration [
687+
// ObjectProperty
688+
/:value Identifier[@name == '${name}']
689+
] /:declaration ObjectExpression
690+
`);
691+
692+
if (res.length > 0) {
693+
const matches: ComponentProperties[] = [];
694+
// this accounts for export const X = {...} and export default {...}
695+
// we need to give the default exported object a name hence '_default'
696+
res.forEach((match) => {
697+
if (
698+
arr.findIndex(
699+
(val) =>
700+
val.name === match.id?.name ||
701+
(val.name === '_default' && !match.id?.name)
702+
) === -1
703+
) {
704+
matches.push({
705+
name: match.id?.name || '_default',
706+
properties: match.init?.properties || match.properties,
707+
});
708+
}
709+
});
710+
711+
arr = [...arr, ...matches];
712+
}
713+
});
714+
return arr;
715+
}
716+
717+
function getComponentDotProperties(
718+
ast: AstQuery,
719+
componentNames: string[]
720+
): ComponentProperties[] {
721+
let arr: ComponentProperties[] = [];
722+
componentNames.forEach((name) => {
723+
const res = ast.query(`
724+
/:program *
725+
// AssignmentExpression[
726+
/:left MemberExpression[
727+
/:object Identifier[@name == '${name}']
599728
]
600-
`);
601-
if (res.length > 0) {
602-
return (
603-
res
604-
// only interested in components that exist
605-
.filter((match) => !componentNames.includes(match))
606-
.map((match) => ({
607-
name: match.id ? match.id.name : '',
608-
properties: match.init?.properties,
609-
}))
610-
);
611-
}
612-
return [];
729+
&&
730+
/:right Identifier
731+
]
732+
`);
733+
if (res.length > 0) {
734+
const properties: ComponentProperties['properties'] = [];
735+
res.forEach((match) => {
736+
if (!componentNames.includes(match.right?.name)) {
737+
return;
738+
}
739+
properties.push({
740+
key: match.left?.property?.name,
741+
value: match.right?.name,
742+
});
743+
});
744+
if (properties.length > 0) {
745+
arr = [
746+
...arr,
747+
{
748+
name,
749+
properties,
750+
},
751+
];
752+
}
753+
}
754+
});
755+
return arr;
613756
}
614757

615758
function getPropTypes(ast: AstQuery, componentName: string): any | undefined {

0 commit comments

Comments
 (0)