Skip to content

Commit 31eae6e

Browse files
authored
fix(types): components.d.ts type resolution for duplicate types (#3337)
this commit updates the type resolution of types used by components when generating a `components.d.ts` file. types that have been deduplicated are now piped to functions used to generate the types for class members that are decorated with `@Prop()`, `@Event()`, and `@Method()`, and used when generating the types for each.
1 parent ed1de3c commit 31eae6e

11 files changed

+627
-27
lines changed

src/compiler/types/generate-app-types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const generateComponentTypesFile = (config: d.Config, buildCtx: d.BuildCtx, areT
7777
* grow as more components (with additional types) are processed.
7878
*/
7979
typeImportData = updateReferenceTypeImports(typeImportData, allTypes, cmp, cmp.sourceFilePath);
80-
return generateComponentTypes(cmp, areTypesInternal);
80+
return generateComponentTypes(cmp, typeImportData, areTypesInternal);
8181
});
8282

8383
c.push(COMPONENTS_DTS_HEADER);

src/compiler/types/generate-component-types.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@ import { generatePropTypes } from './generate-prop-types';
77
/**
88
* Generate a string based on the types that are defined within a component
99
* @param cmp the metadata for the component that a type definition string is generated for
10+
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
1011
* @param areTypesInternal `true` if types being generated are for a project's internal purposes, `false` otherwise
1112
* @returns the generated types string alongside additional metadata
1213
*/
13-
export const generateComponentTypes = (cmp: d.ComponentCompilerMeta, areTypesInternal: boolean): d.TypesModule => {
14+
export const generateComponentTypes = (
15+
cmp: d.ComponentCompilerMeta,
16+
typeImportData: d.TypesImportData,
17+
areTypesInternal: boolean
18+
): d.TypesModule => {
1419
const tagName = cmp.tagName.toLowerCase();
1520
const tagNameAsPascal = dashToPascalCase(tagName);
1621
const htmlElementName = `HTML${tagNameAsPascal}Element`;
1722

18-
const propAttributes = generatePropTypes(cmp);
19-
const methodAttributes = generateMethodTypes(cmp);
20-
const eventAttributes = generateEventTypes(cmp);
23+
const propAttributes = generatePropTypes(cmp, typeImportData);
24+
const methodAttributes = generateMethodTypes(cmp, typeImportData);
25+
const eventAttributes = generateEventTypes(cmp, typeImportData);
2126

2227
const componentAttributes = attributesToMultiLineString(
2328
[...propAttributes, ...methodAttributes],
+28-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import type * as d from '../../declarations';
22
import { getTextDocs, toTitleCase } from '@utils';
3+
import { updateTypeIdentifierNames } from './stencil-types';
34

45
/**
56
* Generates the individual event types for all @Event() decorated events in a component
67
* @param cmpMeta component runtime metadata for a single component
8+
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
79
* @returns the generated type metadata
810
*/
9-
export const generateEventTypes = (cmpMeta: d.ComponentCompilerMeta): d.TypeInfo => {
11+
export const generateEventTypes = (cmpMeta: d.ComponentCompilerMeta, typeImportData: d.TypesImportData): d.TypeInfo => {
1012
return cmpMeta.events.map((cmpEvent) => {
1113
const name = `on${toTitleCase(cmpEvent.name)}`;
12-
const type = cmpEvent.complexType.original
13-
? `(event: CustomEvent<${cmpEvent.complexType.original}>) => void`
14-
: `CustomEvent`;
14+
const type = getEventType(cmpEvent, typeImportData, cmpMeta.sourceFilePath);
1515
return {
1616
name,
1717
type,
@@ -22,3 +22,27 @@ export const generateEventTypes = (cmpMeta: d.ComponentCompilerMeta): d.TypeInfo
2222
};
2323
});
2424
};
25+
26+
/**
27+
* Determine the correct type name for all type(s) used by a class member annotated with `@Event()`
28+
* @param cmpEvent the compiler metadata for a single `@Event()`
29+
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
30+
* @param componentSourcePath the path to the component on disk
31+
* @returns the type associated with a `@Event()`
32+
*/
33+
const getEventType = (
34+
cmpEvent: d.ComponentCompilerEvent,
35+
typeImportData: d.TypesImportData,
36+
componentSourcePath: string
37+
): string => {
38+
if (!cmpEvent.complexType.original) {
39+
return 'CustomEvent';
40+
}
41+
const updatedTypeName = updateTypeIdentifierNames(
42+
cmpEvent.complexType.references,
43+
typeImportData,
44+
componentSourcePath,
45+
cmpEvent.complexType.original
46+
);
47+
return `(event: CustomEvent<${updatedTypeName}>) => void`;
48+
};
+27-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,43 @@
11
import type * as d from '../../declarations';
22
import { getTextDocs } from '@utils';
3+
import { updateTypeIdentifierNames } from './stencil-types';
34

45
/**
56
* Generates the individual event types for all @Method() decorated events in a component
67
* @param cmpMeta component runtime metadata for a single component
8+
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
79
* @returns the generated type metadata
810
*/
9-
export const generateMethodTypes = (cmpMeta: d.ComponentCompilerMeta): d.TypeInfo => {
11+
export const generateMethodTypes = (
12+
cmpMeta: d.ComponentCompilerMeta,
13+
typeImportData: d.TypesImportData
14+
): d.TypeInfo => {
1015
return cmpMeta.methods.map((cmpMethod) => ({
1116
name: cmpMethod.name,
12-
type: cmpMethod.complexType.signature,
17+
type: getType(cmpMethod, typeImportData, cmpMeta.sourceFilePath),
1318
optional: false,
1419
required: false,
1520
internal: cmpMethod.internal,
1621
jsdoc: getTextDocs(cmpMethod.docs),
1722
}));
1823
};
24+
25+
/**
26+
* Determine the correct type name for all type(s) used by a class member annotated with `@Method()`
27+
* @param cmpMethod the compiler metadata for a single `@Method()`
28+
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
29+
* @param componentSourcePath the path to the component on disk
30+
* @returns the type associated with a `@Method()`
31+
*/
32+
function getType(
33+
cmpMethod: d.ComponentCompilerMethod,
34+
typeImportData: d.TypesImportData,
35+
componentSourcePath: string
36+
): string {
37+
return updateTypeIdentifierNames(
38+
cmpMethod.complexType.references,
39+
typeImportData,
40+
componentSourcePath,
41+
cmpMethod.complexType.signature
42+
);
43+
}

src/compiler/types/generate-prop-types.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import type * as d from '../../declarations';
22
import { getTextDocs } from '@utils';
3+
import { updateTypeIdentifierNames } from './stencil-types';
34

45
/**
56
* Generates the individual event types for all @Prop() decorated events in a component
67
* @param cmpMeta component runtime metadata for a single component
8+
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
79
* @returns the generated type metadata
810
*/
9-
export const generatePropTypes = (cmpMeta: d.ComponentCompilerMeta): d.TypeInfo => {
11+
export const generatePropTypes = (cmpMeta: d.ComponentCompilerMeta, typeImportData: d.TypesImportData): d.TypeInfo => {
1012
return [
1113
...cmpMeta.properties.map((cmpProp) => ({
1214
name: cmpProp.name,
13-
type: cmpProp.complexType.original,
15+
type: getType(cmpProp, typeImportData, cmpMeta.sourceFilePath),
1416
optional: cmpProp.optional,
1517
required: cmpProp.required,
1618
internal: cmpProp.internal,
@@ -26,3 +28,23 @@ export const generatePropTypes = (cmpMeta: d.ComponentCompilerMeta): d.TypeInfo
2628
})),
2729
];
2830
};
31+
32+
/**
33+
* Determine the correct type name for all type(s) used by a class member annotated with `@Prop()`
34+
* @param cmpProp the compiler metadata for a single `@Prop()`
35+
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
36+
* @param componentSourcePath the path to the component on disk
37+
* @returns the type associated with a `@Prop()`
38+
*/
39+
function getType(
40+
cmpProp: d.ComponentCompilerProperty,
41+
typeImportData: d.TypesImportData,
42+
componentSourcePath: string
43+
): string {
44+
return updateTypeIdentifierNames(
45+
cmpProp.complexType.references,
46+
typeImportData,
47+
componentSourcePath,
48+
cmpProp.complexType.original
49+
);
50+
}

src/compiler/types/stencil-types.ts

+76-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type * as d from '../../declarations';
2-
import { dirname, join, relative } from 'path';
2+
import { dirname, join, relative, resolve } from 'path';
33
import { isOutputTargetDistTypes } from '../output-targets/output-utils';
44
import { normalizePath } from '@utils';
55

@@ -30,6 +30,81 @@ export const updateStencilTypesImports = (typesDir: string, dtsFilePath: string,
3030
return dtsContent;
3131
};
3232

33+
/**
34+
* Utility for ensuring that naming collisions do not appear in type declaration files for a component's class members
35+
* decorated with @Prop, @Event, and @Method
36+
* @param typeReferences all type names used by a component class member
37+
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
38+
* @param sourceFilePath the path to the source file of a component using the type being inspected
39+
* @param initialType the name of the type that may be updated
40+
* @returns the updated type name, which may be the same as the initial type name provided as an argument to this
41+
* function
42+
*/
43+
export const updateTypeIdentifierNames = (
44+
typeReferences: d.ComponentCompilerTypeReferences,
45+
typeImportData: d.TypesImportData,
46+
sourceFilePath: string,
47+
initialType: string
48+
): string => {
49+
let currentTypeName = initialType;
50+
51+
// iterate over each of the type references, as there may be >1 reference to inspect
52+
for (let typeReference of Object.values(typeReferences)) {
53+
const importResolvedFile = getTypeImportPath(typeReference.path, sourceFilePath);
54+
55+
if (!typeImportData.hasOwnProperty(importResolvedFile)) {
56+
continue;
57+
}
58+
59+
for (let typesImportDatumElement of typeImportData[importResolvedFile]) {
60+
currentTypeName = updateTypeName(currentTypeName, typesImportDatumElement);
61+
}
62+
}
63+
return currentTypeName;
64+
};
65+
66+
/**
67+
* Determine the path of a given type reference, relative to the path of a source file
68+
* @param importResolvedFile the path to the file containing the resolve type. may be absolute or relative
69+
* @param sourceFilePath the component source file path to resolve against
70+
* @returns the path of the type import
71+
*/
72+
const getTypeImportPath = (importResolvedFile: string | undefined, sourceFilePath: string): string => {
73+
const isPathRelative = importResolvedFile && importResolvedFile.startsWith('.');
74+
if (isPathRelative) {
75+
importResolvedFile = resolve(dirname(sourceFilePath), importResolvedFile);
76+
}
77+
78+
return importResolvedFile;
79+
};
80+
81+
/**
82+
* Determine whether the string representation of a type should be replaced with an alias
83+
* @param currentTypeName the current string representation of a type
84+
* @param typeAlias a type member and a potential different name associated with the type member
85+
* @returns the updated string representation of a type. If the type is not updated, the original type name is returned
86+
*/
87+
const updateTypeName = (currentTypeName: string, typeAlias: d.TypesMemberNameData): string => {
88+
if (!typeAlias.importName) {
89+
return currentTypeName;
90+
}
91+
92+
// TODO(STENCIL-419): Update this functionality to no longer use a regex
93+
// negative lookahead specifying that quotes that designate a string in JavaScript cannot follow some expression
94+
const endingStrChar = '(?!("|\'|`))';
95+
/**
96+
* A regular expression that looks at type names along a [word boundary](https://www.regular-expressions.info/wordboundaries.html).
97+
* This is used as the best approximation for replacing type collisions, as this stage of compilation has only
98+
* 'flattened' type information in the form of a String.
99+
*
100+
* This regex should be expected to capture types that are found in generics, unions, intersections, etc., but not
101+
* those in string literals. We do not check for a starting quote (" | ' | `) here as some browsers do not support
102+
* negative lookbehind. This works "well enough" until STENCIL-419 is completed.
103+
*/
104+
const typeNameRegex = new RegExp(`${typeAlias.localName}\\b${endingStrChar}`, 'g');
105+
return currentTypeName.replace(typeNameRegex, typeAlias.importName);
106+
};
107+
33108
/**
34109
* Writes Stencil core typings file to disk for a dist-* output target
35110
* @param config the Stencil configuration associated with the project being compiled
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as d from '@stencil/core/declarations';
2+
3+
/**
4+
* Generates a stub {@link TypesImportData}.
5+
* @param overrides a partial implementation of `TypesImportData`. Any provided fields will override the defaults
6+
* provided by this function.
7+
* @returns the stubbed `TypesImportData`
8+
*/
9+
export const stubTypesImportData = (overrides: Partial<d.TypesImportData> = {}): d.TypesImportData => {
10+
/**
11+
* By design, we do not provide any default values. the keys used in this data structure will be highly dependent on
12+
* the tests being written, and providing default values may lead to unexpected behavior when enumerating the returned
13+
* stub
14+
*/
15+
const defaults: d.TypesImportData = {};
16+
17+
return { ...defaults, ...overrides };
18+
};

0 commit comments

Comments
 (0)