Skip to content

Commit

Permalink
refactor(types): add clarity to type declaration file generation (#3318)
Browse files Browse the repository at this point in the history
this commit adds jsdoc to various internal/private stencil types for
finding typescript types and avoiding naming collisions. the purpose of
this commit is to keep refactoring/chores of this nature separate from
core logic changes - this allows this commit to be made separate from
existing pull requests

this commit refactors the `update-import-refs` helper module by adding
jsdoc and renaming existing variables in the name of improving clarity.
this module utilizes multiple data structures that cover similar aspects
of the type resolution process, but can be difficult to keep a mental
model of. the intent of this refactor is to clarify the intent/purpose
of this code.
  • Loading branch information
rwaskiewicz authored Apr 18, 2022
1 parent bcb11cd commit 43b05a3
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 30 deletions.
7 changes: 7 additions & 0 deletions src/compiler/types/generate-app-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,20 @@ const generateComponentTypesFile = (config: d.Config, buildCtx: d.BuildCtx, areT
const components = buildCtx.components.filter((m) => !m.isCollectionDependency);

const modules: d.TypesModule[] = components.map((cmp) => {
/**
* Generate a key-value store that uses the path to the file where an import is defined as the key, and an object
* containing the import's original name and any 'new' name we give it to avoid collisions. We're generating this
* data structure for each Stencil component in series, therefore the memory footprint of this entity will likely
* grow as more components (with additional types) are processed.
*/
typeImportData = updateReferenceTypeImports(typeImportData, allTypes, cmp, cmp.sourceFilePath);
return generateComponentTypes(cmp, areTypesInternal);
});

c.push(COMPONENTS_DTS_HEADER);
c.push(`import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";`);

// write the import statements for our type declaration file
c.push(
...Object.keys(typeImportData).map((filePath) => {
const typeData = typeImportData[filePath];
Expand Down
83 changes: 56 additions & 27 deletions src/compiler/types/update-import-refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,105 @@ import { dirname, resolve } from 'path';

/**
* Find all referenced types by a component and add them to the `importDataObj` parameter
* @param importDataObj key/value of type import file, each value is an array of imported types
* @param allTypes an output parameter containing a map of seen types and the number of times the type has been seen
* @param importDataObj an output parameter that contains the imported types seen thus far by the compiler
* @param typeCounts a map of seen types and the number of times the type has been seen
* @param cmp the metadata associated with the component whose types are being inspected
* @param filePath the path of the component file
* @returns the updated import data
*/
export const updateReferenceTypeImports = (
importDataObj: d.TypesImportData,
allTypes: Map<string, number>,
typeCounts: Map<string, number>,
cmp: d.ComponentCompilerMeta,
filePath: string
): d.TypesImportData => {
const updateImportReferences = updateImportReferenceFactory(allTypes, filePath);
const updateImportReferences = updateImportReferenceFactory(typeCounts, filePath);

return [...cmp.properties, ...cmp.events, ...cmp.methods]
.filter((cmpProp) => cmpProp.complexType && cmpProp.complexType.references)
.reduce((obj, cmpProp) => {
return updateImportReferences(obj, cmpProp.complexType.references);
.filter(
(cmpProp: d.ComponentCompilerProperty | d.ComponentCompilerEvent | d.ComponentCompilerMethod) =>
cmpProp.complexType && cmpProp.complexType.references
)
.reduce((typesImportData: d.TypesImportData, cmpProp) => {
return updateImportReferences(typesImportData, cmpProp.complexType.references);
}, importDataObj);
};

const updateImportReferenceFactory = (allTypes: Map<string, number>, filePath: string) => {
/**
* Describes a function that updates type import references for a file
* @param existingTypeImportData locally/imported/globally used type names. This data structure keeps track of the name
* of the type that is used in a file, and an alias that Stencil may have generated for the name to avoid collisions.
* @param typesReferences type references from a single file
* @returns updated type import data
*/
type ImportReferenceUpdater = (
existingTypeImportData: d.TypesImportData,
typeReferences: { [key: string]: d.ComponentCompilerTypeReference }
) => d.TypesImportData;

/**
* Factory function to create an `ImportReferenceUpdater` instance
* @param typeCounts a key-value store of seen type names and the number of times the type name has been seen
* @param filePath the path of the file containing the component whose imports are being inspected
* @returns an `ImportReferenceUpdater` instance for updating import references in the provided `filePath`
*/
const updateImportReferenceFactory = (typeCounts: Map<string, number>, filePath: string): ImportReferenceUpdater => {
/**
* Determines the number of times that a type identifier (name) has been used. If an identifier has been used before,
* append the number of times the identifier has been seen to its name to avoid future naming collisions
* @param name the identifier name to check for previous usages
* @returns the identifier name, potentially with an integer appended to its name if it has been seen before.
*/
function getIncrementTypeName(name: string): string {
const counter = allTypes.get(name);
const counter = typeCounts.get(name);
if (counter === undefined) {
allTypes.set(name, 1);
typeCounts.set(name, 1);
return name;
}
allTypes.set(name, counter + 1);
typeCounts.set(name, counter + 1);
return `${name}${counter}`;
}

return (obj: d.TypesImportData, typeReferences: { [key: string]: d.ComponentCompilerTypeReference }) => {
return (
existingTypeImportData: d.TypesImportData,
typeReferences: { [key: string]: d.ComponentCompilerTypeReference }
): d.TypesImportData => {
Object.keys(typeReferences)
.map((typeName) => {
return [typeName, typeReferences[typeName]] as [string, d.ComponentCompilerTypeReference];
return [typeName, typeReferences[typeName]];
})
.forEach(([typeName, type]) => {
let importFileLocation: string;
.forEach(([typeName, typeReference]: [string, d.ComponentCompilerTypeReference]) => {
let importResolvedFile: string;

// If global then there is no import statement needed
if (type.location === 'global') {
if (typeReference.location === 'global') {
return;

// If local then import location is the current file
} else if (type.location === 'local') {
importFileLocation = filePath;
} else if (type.location === 'import') {
importFileLocation = type.path;
} else if (typeReference.location === 'local') {
importResolvedFile = filePath;
} else if (typeReference.location === 'import') {
importResolvedFile = typeReference.path;
}

// If this is a relative path make it absolute
if (importFileLocation.startsWith('.')) {
importFileLocation = resolve(dirname(filePath), importFileLocation);
if (importResolvedFile.startsWith('.')) {
importResolvedFile = resolve(dirname(filePath), importResolvedFile);
}

obj[importFileLocation] = obj[importFileLocation] || [];
existingTypeImportData[importResolvedFile] = existingTypeImportData[importResolvedFile] || [];

// If this file already has a reference to this type move on
if (obj[importFileLocation].find((df) => df.localName === typeName)) {
if (existingTypeImportData[importResolvedFile].find((df) => df.localName === typeName)) {
return;
}

const newTypeName = getIncrementTypeName(typeName);
obj[importFileLocation].push({
existingTypeImportData[importResolvedFile].push({
localName: typeName,
importName: newTypeName,
});
});

return obj;
return existingTypeImportData;
};
};
42 changes: 39 additions & 3 deletions src/declarations/stencil-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -850,12 +850,30 @@ export interface ComponentCompilerPropertyComplexType {
references: ComponentCompilerTypeReferences;
}

export interface ComponentCompilerTypeReferences {
[key: string]: ComponentCompilerTypeReference;
}
/**
* A record of `ComponentCompilerTypeReference` entities.
*
* Each key in this record is intended to be the names of the types used by a component. However, this is not enforced
* by the type system (I.E. any string can be used as a key).
*
* Note any key can be a user defined type or a TypeScript standard type.
*/
export type ComponentCompilerTypeReferences = Record<string, ComponentCompilerTypeReference>;

/**
* Describes a reference to a type used by a component.
*/
export interface ComponentCompilerTypeReference {
/**
* A type may be defined:
* - locally (in the same file as the component that uses it)
* - globally
* - by importing it into a file (and is defined elsewhere)
*/
location: 'local' | 'global' | 'import';
/**
* The path to the type reference, if applicable (global types should not need a path associated with them)
*/
path?: string;
}

Expand Down Expand Up @@ -2428,12 +2446,30 @@ export interface NewSpecPageOptions {
strictBuild?: boolean;
}

/**
* A record of `TypesMemberNameData` entities.
*
* Each key in this record is intended to be the path to a file that declares one or more types used by a component.
* However, this is not enforced by the type system - users of this interface should not make any assumptions regarding
* the format of the path used as a key (relative vs. absolute)
*/
export interface TypesImportData {
[key: string]: TypesMemberNameData[];
}

/**
* A type describing how Stencil may alias an imported type to avoid naming collisions when performing operations such
* as generating `components.d.ts` files.
*/
export interface TypesMemberNameData {
/**
* The name of the type as it's used within a file.
*/
localName: string;
/**
* An alias that Stencil may apply to the `localName` to avoid naming collisions. This name does not appear in the
* file that is using `localName`.
*/
importName?: string;
}

Expand Down

0 comments on commit 43b05a3

Please sign in to comment.