diff --git a/.vscode/settings.json b/.vscode/settings.json index 72446f4..a1e2ab6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,16 @@ { + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/node_modules": true, + "**/dist": true + }, + "search.exclude": { + "**/node_modules": true, + "**/dist": true + }, "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/src/transforms/collapse-intersection-interfaces-transform.ts b/src/transforms/collapse-intersection-interfaces-transform.ts index 30395f9..cca02a4 100644 --- a/src/transforms/collapse-intersection-interfaces-transform.ts +++ b/src/transforms/collapse-intersection-interfaces-transform.ts @@ -1,4 +1,5 @@ import * as ts from 'typescript'; +import * as _ from 'lodash'; import * as helpers from '../helpers'; @@ -13,8 +14,8 @@ import * as helpers from '../helpers'; * type Foo = {foo: string; bar: number;} */ export function collapseIntersectionInterfacesTransformFactoryFactory( - typeChecker: ts.TypeChecker, - ): ts.TransformerFactory { + typeChecker: ts.TypeChecker, +): ts.TransformerFactory { return function collapseIntersectionInterfacesTransformFactory(context: ts.TransformationContext) { return function collapseIntersectionInterfacesTransform(sourceFile: ts.SourceFile) { const visited = ts.visitEachChild(sourceFile, visitor, context); @@ -31,28 +32,121 @@ export function collapseIntersectionInterfacesTransformFactoryFactory( } function visitTypeAliasDeclaration(node: ts.TypeAliasDeclaration) { - if ( - ts.isIntersectionTypeNode(node.type) - && node.type.types.every(ts.isTypeLiteralNode) - ) { - // We need cast `node.type.types` to `ts.NodeArray` - // because TypeScript can't figure out `node.type.types.every(ts.isTypeLiteralNode)` - const allMembers = (node.type.types as ts.NodeArray) - .map((type) => type.members) - .reduce((all, members) => ts.createNodeArray(all.concat(members)), ts.createNodeArray([])); - + if (ts.isIntersectionTypeNode(node.type)) { return ts.createTypeAliasDeclaration( [], [], node.name.text, [], - ts.createTypeLiteralNode(allMembers), + visitIntersectionTypeNode(node.type), ); } return node; } - } - } -} + function visitIntersectionTypeNode(node: ts.IntersectionTypeNode) { + // Only intersection of type literals can be colapsed. + // We are currently ignoring intersections such as `{foo: string} & {bar: string} & TypeRef` + // TODO: handle mix of type references and multiple literal types + if (!node.types.every(typeNode => ts.isTypeLiteralNode(typeNode))) { + return node; + } + + // We need cast `node.type.types` to `ts.NodeArray` + // because TypeScript can't figure out `node.type.types.every(ts.isTypeLiteralNode)` + const types = node.types as ts.NodeArray; + + // Build a map of member names to all of types found in intersectioning type literals + // For instance {foo: string, bar: number} & { foo: number } will result in a map like this: + // Map { + // 'foo' => Set { 'string', 'number' }, + // 'bar' => Set { 'number' } + // } + const membersMap = new Map>(); + + // A sepecial member of type literal nodes is index signitures which don't have a name + // We use this symbol to track it in our members map + const INDEX_SIGNITUTRE_MEMBER = Symbol('Index signiture member'); + + // Keep a reference of first index signiture member parameters. (ignore rest) + let indexMemberParameter: ts.NodeArray | null = null; + + // Iterate through all of type literal nodes members and add them to the members map + types.forEach(typeNode => { + typeNode.members.forEach(member => { + if (ts.isIndexSignatureDeclaration(member)) { + if (member.type !== undefined) { + if (membersMap.has(INDEX_SIGNITUTRE_MEMBER)) { + membersMap.get(INDEX_SIGNITUTRE_MEMBER)!.add(member.type); + } else { + indexMemberParameter = member.parameters; + membersMap.set(INDEX_SIGNITUTRE_MEMBER, new Set([member.type])); + } + } + } else if (ts.isPropertySignature(member)) { + if (member.type !== undefined) { + let memberName = member.name.getText(sourceFile); + + // For unknown reasons, member.name.getText() is returning nothing in some cases + // This is probably because previous transformers did something with the AST that + // index of text string of member identifier is lost + // TODO: investigate + if (!memberName) { + memberName = (member.name as any).escapedText; + } + + if (membersMap.has(memberName)) { + membersMap.get(memberName)!.add(member.type); + } else { + membersMap.set(memberName, new Set([member.type])); + } + } + } + }); + }); + + // Result type literal members list + const finalMembers: Array = []; + + // Put together the map into a type literal that has member per each map entery and type of that + // member is a union of all types in vlues for that member name in members map + // if a member has only one type, create a simple type literal for it + for (const [name, types] of membersMap.entries()) { + if (typeof name === 'symbol') { + continue; + } + // if for this name there is only one type found use the first type, otherwise make a union of all types + let resultType = types.size === 1 ? Array.from(types)[0] : createUnionType(Array.from(types)); + + finalMembers.push(ts.createPropertySignature([], name, undefined, resultType, undefined)); + } + + // Handle index signiture member + if (membersMap.has(INDEX_SIGNITUTRE_MEMBER)) { + const indexTypes = Array.from(membersMap.get(INDEX_SIGNITUTRE_MEMBER)!); + let indexType = indexTypes[0]; + if (indexTypes.length > 1) { + indexType = createUnionType(indexTypes); + } + const indexSigniture = ts.createIndexSignature([], [], indexMemberParameter!, indexType); + finalMembers.push(indexSigniture); + } + + // Generate one single type literal node + return ts.createTypeLiteralNode(finalMembers); + } + + /** + * Create a union type from multiple type nodes + * @param types + */ + function createUnionType(types: ts.TypeNode[]) { + // first dedupe literal types + // TODO: this only works if all types are primitive types like string or number + const uniqueTypes = _.uniqBy(types, type => type.kind); + return ts.createUnionOrIntersectionTypeNode(ts.SyntaxKind.UnionType, uniqueTypes); + } + }; + }; +} diff --git a/src/transforms/react-move-prop-types-to-class-transform.ts b/src/transforms/react-move-prop-types-to-class-transform.ts index 7b6d9a8..d954a11 100644 --- a/src/transforms/react-move-prop-types-to-class-transform.ts +++ b/src/transforms/react-move-prop-types-to-class-transform.ts @@ -33,7 +33,9 @@ export type Factory = ts.TransformerFactory; export function reactMovePropTypesToClassTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory { return function reactMovePropTypesToClassTransformFactory(context: ts.TransformationContext) { return function reactMovePropTypesToClassTransform(sourceFile: ts.SourceFile) { - return visitSourceFile(sourceFile, typeChecker); + const visited = visitSourceFile(sourceFile, typeChecker); + ts.addEmitHelpers(visited, context.readEmitHelpers()); + return visited; }; }; } diff --git a/src/transforms/react-remove-prop-types-assignment-transform.ts b/src/transforms/react-remove-prop-types-assignment-transform.ts index e65ecf1..4b57fa9 100644 --- a/src/transforms/react-remove-prop-types-assignment-transform.ts +++ b/src/transforms/react-remove-prop-types-assignment-transform.ts @@ -15,13 +15,15 @@ export type Factory = ts.TransformerFactory; * After * class SomeComponent extends React.Component<{foo: number;}, {bar: string;}> {} */ -export function reactRemovePropTypesAssignmentTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory{ +export function reactRemovePropTypesAssignmentTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory { return function reactRemovePropTypesAssignmentTransformFactory(context: ts.TransformationContext) { return function reactRemovePropTypesAssignmentTransform(sourceFile: ts.SourceFile) { - return ts.updateSourceFileNode( + const visited = ts.updateSourceFileNode( sourceFile, sourceFile.statements.filter(s => !helpers.isReactPropTypeAssignmentStatement(s)), ); - } - } + ts.addEmitHelpers(visited, context.readEmitHelpers()); + return visited; + }; + }; } diff --git a/src/transforms/react-remove-prop-types-import.ts b/src/transforms/react-remove-prop-types-import.ts index 8710971..8644b8c 100644 --- a/src/transforms/react-remove-prop-types-import.ts +++ b/src/transforms/react-remove-prop-types-import.ts @@ -20,7 +20,7 @@ export type Factory = ts.TransformerFactory; export function reactRemovePropTypesImportTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory { return function reactRemovePropTypesImportTransformFactory(context: ts.TransformationContext) { return function reactRemovePropTypesImportTransform(sourceFile: ts.SourceFile) { - return ts.updateSourceFileNode( + const visited = ts.updateSourceFileNode( sourceFile, sourceFile.statements .filter(s => { @@ -32,6 +32,8 @@ export function reactRemovePropTypesImportTransformFactoryFactory(typeChecker: t }) .map(updateReactImportIfNeeded), ); + ts.addEmitHelpers(visited, context.readEmitHelpers()); + return visited; }; }; } diff --git a/src/transforms/react-remove-static-prop-types-member-transform.ts b/src/transforms/react-remove-static-prop-types-member-transform.ts index e0fc471..7054884 100644 --- a/src/transforms/react-remove-static-prop-types-member-transform.ts +++ b/src/transforms/react-remove-static-prop-types-member-transform.ts @@ -21,7 +21,9 @@ export type Factory = ts.TransformerFactory; export function reactRemoveStaticPropTypesMemberTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory { return function reactRemoveStaticPropTypesMemberTransformFactory(context: ts.TransformationContext) { return function reactRemoveStaticPropTypesMemberTransform(sourceFile: ts.SourceFile) { - return ts.visitEachChild(sourceFile, visitor, context); + const visited = ts.visitEachChild(sourceFile, visitor, context); + ts.addEmitHelpers(visited, context.readEmitHelpers()); + return visited; function visitor(node: ts.Node) { if (ts.isClassDeclaration(node) && helpers.isReactComponent(node, typeChecker)) { @@ -32,29 +34,29 @@ export function reactRemoveStaticPropTypesMemberTransformFactoryFactory(typeChec node.name, node.typeParameters, ts.createNodeArray(node.heritageClauses), - node.members.filter((member) => { + node.members.filter(member => { if ( - ts.isPropertyDeclaration(member) - && helpers.hasStaticModifier(member) - && helpers.isPropTypesMember(member, sourceFile) + ts.isPropertyDeclaration(member) && + helpers.hasStaticModifier(member) && + helpers.isPropTypesMember(member, sourceFile) ) { return false; } // propTypes getter if ( - ts.isGetAccessorDeclaration(member) - && helpers.hasStaticModifier(member) - && helpers.isPropTypesMember(member, sourceFile) + ts.isGetAccessorDeclaration(member) && + helpers.hasStaticModifier(member) && + helpers.isPropTypesMember(member, sourceFile) ) { return false; } return true; }), - ) + ); } return node; } - } - } + }; + }; } diff --git a/src/transforms/react-stateless-function-make-props-transform.ts b/src/transforms/react-stateless-function-make-props-transform.ts index 76bd40f..0222cec 100644 --- a/src/transforms/react-stateless-function-make-props-transform.ts +++ b/src/transforms/react-stateless-function-make-props-transform.ts @@ -36,7 +36,9 @@ export type Factory = ts.TransformerFactory; export function reactStatelessFunctionMakePropsTransformFactoryFactory(typeChecker: ts.TypeChecker): Factory { return function reactStatelessFunctionMakePropsTransformFactory(context: ts.TransformationContext) { return function reactStatelessFunctionMakePropsTransform(sourceFile: ts.SourceFile) { - return visitSourceFile(sourceFile, typeChecker); + const visited = visitSourceFile(sourceFile, typeChecker); + ts.addEmitHelpers(visited, context.readEmitHelpers()); + return visited; }; }; } diff --git a/test/collapse-intersection-interfaces-transform/repeated/input.tsx b/test/collapse-intersection-interfaces-transform/repeated/input.tsx new file mode 100644 index 0000000..2bfb4e6 --- /dev/null +++ b/test/collapse-intersection-interfaces-transform/repeated/input.tsx @@ -0,0 +1,5 @@ +type A = { foo: string; } & { foo: string; }; + +type B = { foo: string; bar: number; } & { foo: number; bar: number; } + +type C = { foo: string; bar: number; } & { foo: number; bar: number; } & { foo: string; } diff --git a/test/collapse-intersection-interfaces-transform/repeated/output.tsx b/test/collapse-intersection-interfaces-transform/repeated/output.tsx new file mode 100644 index 0000000..5dc0eec --- /dev/null +++ b/test/collapse-intersection-interfaces-transform/repeated/output.tsx @@ -0,0 +1,13 @@ +type A = { + foo: string, +}; + +type B = { + foo: string | number, + bar: number, +}; + +type C = { + foo: string | number, + bar: number, +}; diff --git a/tsconfig.json b/tsconfig.json index 73cc4e3..0a2ed71 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,11 +7,12 @@ "module": "commonjs", "emitDecoratorMetadata": true, "experimentalDecorators": true, + "downlevelIteration": true, "sourceMap": true, "outDir": "dist", - "sourceRoot": "../src" + "sourceRoot": "../src", + "lib": ["dom", "es2015"] }, "exclude": ["node_modules", "test", "dist"], - "types": ["node", "jest"], - "lib": ["es2017"] + "types": ["node", "jest"] }