Skip to content

Commit

Permalink
fix(types): components.d.ts type resolution for duplicate types (#3337)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rwaskiewicz authored Apr 22, 2022
1 parent ed1de3c commit 31eae6e
Show file tree
Hide file tree
Showing 11 changed files with 627 additions and 27 deletions.
2 changes: 1 addition & 1 deletion src/compiler/types/generate-app-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const generateComponentTypesFile = (config: d.Config, buildCtx: d.BuildCtx, areT
* grow as more components (with additional types) are processed.
*/
typeImportData = updateReferenceTypeImports(typeImportData, allTypes, cmp, cmp.sourceFilePath);
return generateComponentTypes(cmp, areTypesInternal);
return generateComponentTypes(cmp, typeImportData, areTypesInternal);
});

c.push(COMPONENTS_DTS_HEADER);
Expand Down
13 changes: 9 additions & 4 deletions src/compiler/types/generate-component-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@ import { generatePropTypes } from './generate-prop-types';
/**
* Generate a string based on the types that are defined within a component
* @param cmp the metadata for the component that a type definition string is generated for
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
* @param areTypesInternal `true` if types being generated are for a project's internal purposes, `false` otherwise
* @returns the generated types string alongside additional metadata
*/
export const generateComponentTypes = (cmp: d.ComponentCompilerMeta, areTypesInternal: boolean): d.TypesModule => {
export const generateComponentTypes = (
cmp: d.ComponentCompilerMeta,
typeImportData: d.TypesImportData,
areTypesInternal: boolean
): d.TypesModule => {
const tagName = cmp.tagName.toLowerCase();
const tagNameAsPascal = dashToPascalCase(tagName);
const htmlElementName = `HTML${tagNameAsPascal}Element`;

const propAttributes = generatePropTypes(cmp);
const methodAttributes = generateMethodTypes(cmp);
const eventAttributes = generateEventTypes(cmp);
const propAttributes = generatePropTypes(cmp, typeImportData);
const methodAttributes = generateMethodTypes(cmp, typeImportData);
const eventAttributes = generateEventTypes(cmp, typeImportData);

const componentAttributes = attributesToMultiLineString(
[...propAttributes, ...methodAttributes],
Expand Down
32 changes: 28 additions & 4 deletions src/compiler/types/generate-event-types.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import type * as d from '../../declarations';
import { getTextDocs, toTitleCase } from '@utils';
import { updateTypeIdentifierNames } from './stencil-types';

/**
* Generates the individual event types for all @Event() decorated events in a component
* @param cmpMeta component runtime metadata for a single component
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
* @returns the generated type metadata
*/
export const generateEventTypes = (cmpMeta: d.ComponentCompilerMeta): d.TypeInfo => {
export const generateEventTypes = (cmpMeta: d.ComponentCompilerMeta, typeImportData: d.TypesImportData): d.TypeInfo => {
return cmpMeta.events.map((cmpEvent) => {
const name = `on${toTitleCase(cmpEvent.name)}`;
const type = cmpEvent.complexType.original
? `(event: CustomEvent<${cmpEvent.complexType.original}>) => void`
: `CustomEvent`;
const type = getEventType(cmpEvent, typeImportData, cmpMeta.sourceFilePath);
return {
name,
type,
Expand All @@ -22,3 +22,27 @@ export const generateEventTypes = (cmpMeta: d.ComponentCompilerMeta): d.TypeInfo
};
});
};

/**
* Determine the correct type name for all type(s) used by a class member annotated with `@Event()`
* @param cmpEvent the compiler metadata for a single `@Event()`
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
* @param componentSourcePath the path to the component on disk
* @returns the type associated with a `@Event()`
*/
const getEventType = (
cmpEvent: d.ComponentCompilerEvent,
typeImportData: d.TypesImportData,
componentSourcePath: string
): string => {
if (!cmpEvent.complexType.original) {
return 'CustomEvent';
}
const updatedTypeName = updateTypeIdentifierNames(
cmpEvent.complexType.references,
typeImportData,
componentSourcePath,
cmpEvent.complexType.original
);
return `(event: CustomEvent<${updatedTypeName}>) => void`;
};
29 changes: 27 additions & 2 deletions src/compiler/types/generate-method-types.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
import type * as d from '../../declarations';
import { getTextDocs } from '@utils';
import { updateTypeIdentifierNames } from './stencil-types';

/**
* Generates the individual event types for all @Method() decorated events in a component
* @param cmpMeta component runtime metadata for a single component
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
* @returns the generated type metadata
*/
export const generateMethodTypes = (cmpMeta: d.ComponentCompilerMeta): d.TypeInfo => {
export const generateMethodTypes = (
cmpMeta: d.ComponentCompilerMeta,
typeImportData: d.TypesImportData
): d.TypeInfo => {
return cmpMeta.methods.map((cmpMethod) => ({
name: cmpMethod.name,
type: cmpMethod.complexType.signature,
type: getType(cmpMethod, typeImportData, cmpMeta.sourceFilePath),
optional: false,
required: false,
internal: cmpMethod.internal,
jsdoc: getTextDocs(cmpMethod.docs),
}));
};

/**
* Determine the correct type name for all type(s) used by a class member annotated with `@Method()`
* @param cmpMethod the compiler metadata for a single `@Method()`
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
* @param componentSourcePath the path to the component on disk
* @returns the type associated with a `@Method()`
*/
function getType(
cmpMethod: d.ComponentCompilerMethod,
typeImportData: d.TypesImportData,
componentSourcePath: string
): string {
return updateTypeIdentifierNames(
cmpMethod.complexType.references,
typeImportData,
componentSourcePath,
cmpMethod.complexType.signature
);
}
26 changes: 24 additions & 2 deletions src/compiler/types/generate-prop-types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import type * as d from '../../declarations';
import { getTextDocs } from '@utils';
import { updateTypeIdentifierNames } from './stencil-types';

/**
* Generates the individual event types for all @Prop() decorated events in a component
* @param cmpMeta component runtime metadata for a single component
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
* @returns the generated type metadata
*/
export const generatePropTypes = (cmpMeta: d.ComponentCompilerMeta): d.TypeInfo => {
export const generatePropTypes = (cmpMeta: d.ComponentCompilerMeta, typeImportData: d.TypesImportData): d.TypeInfo => {
return [
...cmpMeta.properties.map((cmpProp) => ({
name: cmpProp.name,
type: cmpProp.complexType.original,
type: getType(cmpProp, typeImportData, cmpMeta.sourceFilePath),
optional: cmpProp.optional,
required: cmpProp.required,
internal: cmpProp.internal,
Expand All @@ -26,3 +28,23 @@ export const generatePropTypes = (cmpMeta: d.ComponentCompilerMeta): d.TypeInfo
})),
];
};

/**
* Determine the correct type name for all type(s) used by a class member annotated with `@Prop()`
* @param cmpProp the compiler metadata for a single `@Prop()`
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
* @param componentSourcePath the path to the component on disk
* @returns the type associated with a `@Prop()`
*/
function getType(
cmpProp: d.ComponentCompilerProperty,
typeImportData: d.TypesImportData,
componentSourcePath: string
): string {
return updateTypeIdentifierNames(
cmpProp.complexType.references,
typeImportData,
componentSourcePath,
cmpProp.complexType.original
);
}
77 changes: 76 additions & 1 deletion src/compiler/types/stencil-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type * as d from '../../declarations';
import { dirname, join, relative } from 'path';
import { dirname, join, relative, resolve } from 'path';
import { isOutputTargetDistTypes } from '../output-targets/output-utils';
import { normalizePath } from '@utils';

Expand Down Expand Up @@ -30,6 +30,81 @@ export const updateStencilTypesImports = (typesDir: string, dtsFilePath: string,
return dtsContent;
};

/**
* Utility for ensuring that naming collisions do not appear in type declaration files for a component's class members
* decorated with @Prop, @Event, and @Method
* @param typeReferences all type names used by a component class member
* @param typeImportData locally/imported/globally used type names, which may be used to prevent naming collisions
* @param sourceFilePath the path to the source file of a component using the type being inspected
* @param initialType the name of the type that may be updated
* @returns the updated type name, which may be the same as the initial type name provided as an argument to this
* function
*/
export const updateTypeIdentifierNames = (
typeReferences: d.ComponentCompilerTypeReferences,
typeImportData: d.TypesImportData,
sourceFilePath: string,
initialType: string
): string => {
let currentTypeName = initialType;

// iterate over each of the type references, as there may be >1 reference to inspect
for (let typeReference of Object.values(typeReferences)) {
const importResolvedFile = getTypeImportPath(typeReference.path, sourceFilePath);

if (!typeImportData.hasOwnProperty(importResolvedFile)) {
continue;
}

for (let typesImportDatumElement of typeImportData[importResolvedFile]) {
currentTypeName = updateTypeName(currentTypeName, typesImportDatumElement);
}
}
return currentTypeName;
};

/**
* Determine the path of a given type reference, relative to the path of a source file
* @param importResolvedFile the path to the file containing the resolve type. may be absolute or relative
* @param sourceFilePath the component source file path to resolve against
* @returns the path of the type import
*/
const getTypeImportPath = (importResolvedFile: string | undefined, sourceFilePath: string): string => {
const isPathRelative = importResolvedFile && importResolvedFile.startsWith('.');
if (isPathRelative) {
importResolvedFile = resolve(dirname(sourceFilePath), importResolvedFile);
}

return importResolvedFile;
};

/**
* Determine whether the string representation of a type should be replaced with an alias
* @param currentTypeName the current string representation of a type
* @param typeAlias a type member and a potential different name associated with the type member
* @returns the updated string representation of a type. If the type is not updated, the original type name is returned
*/
const updateTypeName = (currentTypeName: string, typeAlias: d.TypesMemberNameData): string => {
if (!typeAlias.importName) {
return currentTypeName;
}

// TODO(STENCIL-419): Update this functionality to no longer use a regex
// negative lookahead specifying that quotes that designate a string in JavaScript cannot follow some expression
const endingStrChar = '(?!("|\'|`))';
/**
* A regular expression that looks at type names along a [word boundary](https://www.regular-expressions.info/wordboundaries.html).
* This is used as the best approximation for replacing type collisions, as this stage of compilation has only
* 'flattened' type information in the form of a String.
*
* This regex should be expected to capture types that are found in generics, unions, intersections, etc., but not
* those in string literals. We do not check for a starting quote (" | ' | `) here as some browsers do not support
* negative lookbehind. This works "well enough" until STENCIL-419 is completed.
*/
const typeNameRegex = new RegExp(`${typeAlias.localName}\\b${endingStrChar}`, 'g');
return currentTypeName.replace(typeNameRegex, typeAlias.importName);
};

/**
* Writes Stencil core typings file to disk for a dist-* output target
* @param config the Stencil configuration associated with the project being compiled
Expand Down
18 changes: 18 additions & 0 deletions src/compiler/types/tests/TypesImportData.stub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as d from '@stencil/core/declarations';

/**
* Generates a stub {@link TypesImportData}.
* @param overrides a partial implementation of `TypesImportData`. Any provided fields will override the defaults
* provided by this function.
* @returns the stubbed `TypesImportData`
*/
export const stubTypesImportData = (overrides: Partial<d.TypesImportData> = {}): d.TypesImportData => {
/**
* By design, we do not provide any default values. the keys used in this data structure will be highly dependent on
* the tests being written, and providing default values may lead to unexpected behavior when enumerating the returned
* stub
*/
const defaults: d.TypesImportData = {};

return { ...defaults, ...overrides };
};
Loading

0 comments on commit 31eae6e

Please sign in to comment.