From 1fe7d864fc6b5858a800340de171247f01450193 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 29 Jul 2020 08:21:26 +0200 Subject: [PATCH 1/6] Refactor ttp#generate from single, overloaded function to many smaller ones --- .../typescript-to-proptypes/src/generator.ts | 327 +++++++++--------- .../typescript-to-proptypes/src/injector.ts | 20 +- .../typescript-to-proptypes/src/parser.ts | 86 ++--- .../src/types/component.ts | 19 + .../src/types/index.ts | 21 +- .../src/types/nodes/baseNodes.ts | 41 --- .../src/types/nodes/component.ts | 25 -- .../src/types/nodes/program.ts | 19 - .../src/types/program.ts | 11 + .../src/types/propTypeDefinition.ts | 28 ++ .../src/types/props.ts | 204 +++++++++++ .../src/types/props/DOMElement.ts | 18 - .../src/types/props/any.ts | 13 - .../src/types/props/array.ts | 18 - .../src/types/props/boolean.ts | 13 - .../src/types/props/element.ts | 19 - .../src/types/props/function.ts | 13 - .../src/types/props/instanceOf.ts | 18 - .../src/types/props/interface.ts | 16 - .../src/types/props/literal.ts | 20 -- .../src/types/props/numeric.ts | 13 - .../src/types/props/object.ts | 13 - .../src/types/props/string.ts | 13 - .../src/types/props/undefined.ts | 13 - .../src/types/props/union.ts | 52 --- .../test/index.test.ts | 8 +- .../test/sort-unions/options.ts | 4 +- scripts/generateProptypes.ts | 4 +- 28 files changed, 493 insertions(+), 576 deletions(-) create mode 100644 packages/typescript-to-proptypes/src/types/component.ts delete mode 100644 packages/typescript-to-proptypes/src/types/nodes/baseNodes.ts delete mode 100644 packages/typescript-to-proptypes/src/types/nodes/component.ts delete mode 100644 packages/typescript-to-proptypes/src/types/nodes/program.ts create mode 100644 packages/typescript-to-proptypes/src/types/program.ts create mode 100644 packages/typescript-to-proptypes/src/types/propTypeDefinition.ts create mode 100644 packages/typescript-to-proptypes/src/types/props.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/DOMElement.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/any.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/array.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/boolean.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/element.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/function.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/instanceOf.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/interface.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/literal.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/numeric.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/object.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/string.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/undefined.ts delete mode 100644 packages/typescript-to-proptypes/src/types/props/union.ts diff --git a/packages/typescript-to-proptypes/src/generator.ts b/packages/typescript-to-proptypes/src/generator.ts index 214e48b87cb306..b9a1af154f0252 100644 --- a/packages/typescript-to-proptypes/src/generator.ts +++ b/packages/typescript-to-proptypes/src/generator.ts @@ -6,7 +6,7 @@ export interface GenerateOptions { * Enable/disable the default sorting (ascending) or provide your own sort function * @default true */ - sortProptypes?: boolean | ((a: t.PropTypeNode, b: t.PropTypeNode) => 0 | -1 | 1); + sortProptypes?: boolean | ((a: t.PropTypeDefinition, b: t.PropTypeDefinition) => 0 | -1 | 1); /** * The name used when importing prop-types @@ -32,7 +32,7 @@ export interface GenerateOptions { * @default Uses `generated` source */ reconcilePropTypes?( - proptype: t.PropTypeNode, + proptype: t.PropTypeDefinition, previous: string | undefined, generated: string, ): string; @@ -41,7 +41,7 @@ export interface GenerateOptions { * Control which PropTypes are included in the final result * @param proptype The current PropType about to be converted to text */ - shouldInclude?(proptype: t.PropTypeNode): boolean | undefined; + shouldInclude?(proptype: t.PropTypeDefinition): boolean | undefined; /** * A comment that will be added to the start of the PropTypes code block @@ -57,25 +57,19 @@ export interface GenerateOptions { * If `undefined` is returned the default `sortLiteralUnions` will be used. */ getSortLiteralUnions?: ( - component: t.ComponentNode, - propType: t.PropTypeNode, - ) => ((a: t.LiteralNode, b: t.LiteralNode) => number) | undefined; + component: t.Component, + propType: t.PropTypeDefinition, + ) => ((a: t.LiteralType, b: t.LiteralType) => number) | undefined; /** * By default literals in unions are sorted by: * - numbers last, ascending * - anything else by their stringified value using localeCompare */ - sortLiteralUnions?: (a: t.LiteralNode, b: t.LiteralNode) => number; - - /** - * The component of the given `node`. - * Must be defined for anything but programs and components - */ - component?: t.ComponentNode; + sortLiteralUnions?: (a: t.LiteralType, b: t.LiteralType) => number; } -function defaultSortLiteralUnions(a: t.LiteralNode, b: t.LiteralNode) { +function defaultSortLiteralUnions(a: t.LiteralType, b: t.LiteralType) { const { value: valueA } = a; const { value: valueB } = b; // numbers ascending @@ -94,24 +88,23 @@ function defaultSortLiteralUnions(a: t.LiteralNode, b: t.LiteralNode) { } /** - * Generates code from the given node - * @param node The node to convert to code + * Generates code from the given component + * @param component The component to convert to code * @param options The options used to control the way the code gets generated */ -export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptions = {}): string { +export function generate(component: t.Component, options: GenerateOptions = {}): string { const { - component, sortProptypes = true, importedName = 'PropTypes', includeJSDoc = true, previousPropTypesSource = new Map(), - reconcilePropTypes = (_prop: t.PropTypeNode, _previous: string, generated: string) => generated, + reconcilePropTypes = (_prop: t.PropTypeDefinition, _previous: string, generated: string) => + generated, shouldInclude, getSortLiteralUnions = () => defaultSortLiteralUnions, - sortLiteralUnions = defaultSortLiteralUnions, } = options; - function jsDoc(documentedNode: t.PropTypeNode | t.LiteralNode) { + function jsDoc(documentedNode: t.PropTypeDefinition | t.LiteralType): string { if (!includeJSDoc || !documentedNode.jsDoc) { return ''; } @@ -120,132 +113,57 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio .reduce((prev, curr) => `${prev}\n* ${curr}`)}\n*/\n`; } - if (Array.isArray(node)) { - const propTypes = node.slice(); - - if (typeof sortProptypes === 'function') { - propTypes.sort(sortProptypes); - } else if (sortProptypes === true) { - propTypes.sort((a, b) => a.name.localeCompare(b.name)); + function generatePropType( + propType: t.PropType, + context: { component: t.Component; propTypeDefinition: t.PropTypeDefinition }, + ): string { + if (propType.type === 'InterfaceNode') { + return `${importedName}.shape({\n${propType.types + .map((type) => generatePropType(type, context)) + .join('\n')}\n})`; } - let filteredNodes = propTypes; - if (shouldInclude) { - filteredNodes = filteredNodes.filter((x) => shouldInclude(x)); + if (propType.type === 'FunctionNode') { + return `${importedName}.func`; } - if (filteredNodes.length === 0) { - return ''; + if (propType.type === 'StringNode') { + return `${importedName}.string`; } - return filteredNodes - .map((prop) => generate(prop, options)) - .reduce((prev, curr) => `${prev}\n${curr}`); - } - - if (t.isProgramNode(node)) { - return node.body - .map((prop) => generate(prop, options)) - .reduce((prev, curr) => `${prev}\n${curr}`); - } - - if (t.isComponentNode(node)) { - const generated = generate(node.types, { ...options, component: node }); - if (generated.length === 0) { - return ''; + if (propType.type === 'boolean') { + return `${importedName}.bool`; } - const comment = - options.comment && - `// ${options.comment.split(/\r?\n/gm).reduce((prev, curr) => `${prev}\n// ${curr}`)}\n`; - - return `${node.name}.propTypes = {\n${comment !== undefined ? comment : ''}${generated}\n}`; - } - - if (component === undefined) { - throw new TypeError('Missing component context. This is likely a bug. Please open an issue.'); - } - - if (t.isPropTypeNode(node)) { - let isOptional = false; - let propType = { ...node.propType }; - - if (t.isUnionNode(propType) && propType.types.some(t.isUndefinedNode)) { - isOptional = true; - propType.types = propType.types.filter( - (prop) => !t.isUndefinedNode(prop) && !(t.isLiteralNode(prop) && prop.value === 'null'), - ); - if (propType.types.length === 1 && t.isLiteralNode(propType.types[0]) === false) { - propType = propType.types[0]; - } + if (propType.type === 'NumericNode') { + return `${importedName}.number`; } - if (t.isDOMElementNode(propType)) { - propType.optional = isOptional; - // Handled internally in the validate function - isOptional = true; + if (propType.type === 'LiteralNode') { + return `${importedName}.oneOf([${jsDoc(propType)}${propType.value}])`; } - const validatorSource = reconcilePropTypes( - node, - previousPropTypesSource.get(node.name), - `${generate(propType, { - ...options, - sortLiteralUnions: getSortLiteralUnions(component, node) || sortLiteralUnions, - })}${isOptional ? '' : '.isRequired'}`, - ); - - return `${jsDoc(node)}"${node.name}": ${validatorSource},`; - } - - if (t.isInterfaceNode(node)) { - return `${importedName}.shape({\n${generate(node.types, { - ...options, - shouldInclude: undefined, - })}\n})`; - } - - if (t.isFunctionNode(node)) { - return `${importedName}.func`; - } - - if (t.isStringNode(node)) { - return `${importedName}.string`; - } - - if (t.isBooleanNode(node)) { - return `${importedName}.bool`; - } - - if (t.isNumericNode(node)) { - return `${importedName}.number`; - } - - if (t.isLiteralNode(node)) { - return `${importedName}.oneOf([${jsDoc(node)}${node.value}])`; - } - - if (t.isObjectNode(node)) { - return `${importedName}.object`; - } + if (propType.type === 'ObjectNode') { + return `${importedName}.object`; + } - if (t.isAnyNode(node)) { - return `${importedName}.any`; - } + if (propType.type === 'any') { + return `${importedName}.any`; + } - if (t.isElementNode(node)) { - return `${importedName}.${node.elementType}`; - } + if (propType.type === 'ElementNode') { + return `${importedName}.${propType.elementType}`; + } - if (t.isInstanceOfNode(node)) { - return `${importedName}.instanceOf(${node.instance})`; - } + if (propType.type === 'InstanceOfNode') { + return `${importedName}.instanceOf(${propType.instance})`; + } - if (t.isDOMElementNode(node)) { - return `function (props, propName) { + if (propType.type === 'DOMElementNode') { + return `function (props, propName) { if (props[propName] == null) { return ${ - node.optional + propType.optional ? 'null' : `new Error("Prop '" + propName + "' is required but wasn't specified")` } @@ -253,60 +171,137 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio return new Error("Expected prop '" + propName + "' to be of type Element") } }`; - } + } - if (t.isArrayNode(node)) { - if (t.isAnyNode(node.arrayType)) { - return `${importedName}.array`; + if (propType.type === 'array') { + if (propType.arrayType.type === 'any') { + return `${importedName}.array`; + } + + return `${importedName}.arrayOf(${generatePropType(propType.arrayType, context)})`; } - return `${importedName}.arrayOf(${generate(node.arrayType, options)})`; - } + if (propType.type === 'UnionNode') { + let [literals, rest] = _.partition( + t.uniqueUnionTypes(propType).types, + (type): type is t.LiteralType => type.type === 'LiteralNode', + ); + + const sortLiteralUnions = + getSortLiteralUnions(context.component, context.propTypeDefinition) || + defaultSortLiteralUnions; + literals = literals.sort(sortLiteralUnions); + + const nodeToStringName = (type: t.PropType): string => { + if (type.type === 'InstanceOfNode') { + return `${type.type}.${type.instance}`; + } + if (type.type === 'InterfaceNode') { + // An interface is PropTypes.shape + // Use `ShapeNode` to get it sorted in the correct order + return `ShapeNode`; + } - if (t.isUnionNode(node)) { - let [literals, rest] = _.partition(t.uniqueUnionTypes(node).types, t.isLiteralNode); + return type.type; + }; - literals = literals.sort(sortLiteralUnions); + rest = rest.sort((a, b) => nodeToStringName(a).localeCompare(nodeToStringName(b))); - const nodeToStringName = (obj: t.Node): string => { - if (t.isInstanceOfNode(obj)) { - return `${obj.type}.${obj.instance}`; - } - if (t.isInterfaceNode(obj)) { - // An interface is PropTypes.shape - // Use `ShapeNode` to get it sorted in the correct order - return `ShapeNode`; + if (literals.find((x) => x.value === 'true') && literals.find((x) => x.value === 'false')) { + rest.push(t.createBooleanType()); + literals = literals.filter((x) => x.value !== 'true' && x.value !== 'false'); } - return obj.type; - }; + const literalProps = + literals.length !== 0 + ? `${importedName}.oneOf([${literals + .map((x) => `${jsDoc(x)}${x.value}`) + .reduce((prev, curr) => `${prev},${curr}`)}])` + : ''; - rest = rest.sort((a, b) => nodeToStringName(a).localeCompare(nodeToStringName(b))); + if (rest.length === 0) { + return literalProps; + } + + if (literals.length === 0 && rest.length === 1) { + return generatePropType(rest[0], context); + } - if (literals.find((x) => x.value === 'true') && literals.find((x) => x.value === 'false')) { - rest.push(t.booleanNode()); - literals = literals.filter((x) => x.value !== 'true' && x.value !== 'false'); + return `${importedName}.oneOfType([${ + literalProps ? `${literalProps}, ` : '' + }${rest + .map((type) => generatePropType(type, context)) + .reduce((prev, curr) => `${prev},${curr}`)}])`; } - const literalProps = - literals.length !== 0 - ? `${importedName}.oneOf([${literals - .map((x) => `${jsDoc(x)}${x.value}`) - .reduce((prev, curr) => `${prev},${curr}`)}])` - : ''; + throw new Error(`Nothing to handle node of type "${propType.type}"`); + } + + function generatePropTypeDefinition( + propTypeDefinition: t.PropTypeDefinition, + context: { component: t.Component }, + ): string { + let isOptional = false; + let propType = { ...propTypeDefinition.propType }; - if (rest.length === 0) { - return literalProps; + if ( + propType.type === 'UnionNode' && + propType.types.some((type) => type.type === 'UndefinedNode') + ) { + isOptional = true; + propType.types = propType.types.filter( + (type) => + type.type !== 'UndefinedNode' && !(type.type === 'LiteralNode' && type.value === 'null'), + ); + if (propType.types.length === 1 && propType.types[0].type !== 'UndefinedNode') { + propType = propType.types[0]; + } } - if (literals.length === 0 && rest.length === 1) { - return generate(rest[0], options); + if (propType.type === 'DOMElementNode') { + propType.optional = isOptional; + // Handled internally in the validate function + isOptional = true; } - return `${importedName}.oneOfType([${literalProps ? `${literalProps}, ` : ''}${rest - .map((x) => generate(x, options)) - .reduce((prev, curr) => `${prev},${curr}`)}])`; + const validatorSource = reconcilePropTypes( + propTypeDefinition, + previousPropTypesSource.get(propTypeDefinition.name), + `${generatePropType(propType, { component: context.component, propTypeDefinition })}${ + isOptional ? '' : '.isRequired' + }`, + ); + + return `${jsDoc(propTypeDefinition)}"${propTypeDefinition.name}": ${validatorSource},`; + } + + const propTypes = component.types.slice(); + + if (typeof sortProptypes === 'function') { + propTypes.sort(sortProptypes); + } else if (sortProptypes === true) { + propTypes.sort((a, b) => a.name.localeCompare(b.name)); + } + + let filteredNodes = propTypes; + if (shouldInclude) { + filteredNodes = filteredNodes.filter((type) => shouldInclude(type)); } - throw new Error(`Nothing to handle node of type "${node.type}"`); + if (filteredNodes.length === 0) { + return ''; + } + + const generated = filteredNodes + .map((prop) => generatePropTypeDefinition(prop, { component })) + .reduce((prev, curr) => `${prev}\n${curr}`); + if (generated.length === 0) { + return ''; + } + + const comment = + options.comment && + `// ${options.comment.split(/\r?\n/gm).reduce((prev, curr) => `${prev}\n// ${curr}`)}\n`; + + return `${component.name}.propTypes = {\n${comment !== undefined ? comment : ''}${generated}\n}`; } diff --git a/packages/typescript-to-proptypes/src/injector.ts b/packages/typescript-to-proptypes/src/injector.ts index c336f9331f6ffc..3b1a3eec722d28 100644 --- a/packages/typescript-to-proptypes/src/injector.ts +++ b/packages/typescript-to-proptypes/src/injector.ts @@ -1,7 +1,7 @@ import * as babel from '@babel/core'; import * as babelTypes from '@babel/types'; import { v4 as uuid } from 'uuid'; -import * as t from './types/index'; +import * as t from './types'; import { generate, GenerateOptions } from './generator'; export type InjectOptions = { @@ -22,8 +22,8 @@ export type InjectOptions = { * @default includeUnusedProps ? true : data.usedProps.includes(data.prop.name) */ shouldInclude?(data: { - component: t.ComponentNode; - prop: t.PropTypeNode; + component: t.Component; + prop: t.PropTypeDefinition; usedProps: string[]; }): boolean | undefined; @@ -38,9 +38,9 @@ export type InjectOptions = { * By always returning 0 from the sort function you keep the order the type checker dictates. */ getSortLiteralUnions?: ( - component: t.ComponentNode, - propType: t.PropTypeNode, - ) => ((a: t.LiteralNode, b: t.LiteralNode) => number) | undefined; + component: t.Component, + propType: t.PropTypeDefinition, + ) => ((a: t.LiteralType, b: t.LiteralType) => number) | undefined; /** * Options passed to babel.transformSync @@ -115,14 +115,14 @@ function getUsedProps( } function plugin( - propTypes: t.ProgramNode, + propTypes: t.Program, options: InjectOptions = {}, mapOfPropTypes: Map, ): babel.PluginObj { const { includeUnusedProps = false, reconcilePropTypes = ( - _prop: t.PropTypeNode, + _prop: t.PropTypeDefinition, _previous: string | undefined, generated: string, ) => generated, @@ -149,7 +149,7 @@ function plugin( function injectPropTypes(injectOptions: { path: babel.NodePath; usedProps: string[]; - props: t.ComponentNode; + props: t.Component; nodeName: string; }) { const { path, props, usedProps, nodeName } = injectOptions; @@ -367,7 +367,7 @@ function plugin( * @param options Options controlling the final result */ export function inject( - propTypes: t.ProgramNode, + propTypes: t.Program, target: string, options: InjectOptions = {}, ): string | null { diff --git a/packages/typescript-to-proptypes/src/parser.ts b/packages/typescript-to-proptypes/src/parser.ts index b9ce6dfc31a19a..7b017bff6a2cd9 100644 --- a/packages/typescript-to-proptypes/src/parser.ts +++ b/packages/typescript-to-proptypes/src/parser.ts @@ -37,7 +37,7 @@ export interface ParserOptions { * @param files The files to later be parsed with `parseFromProgram` * @param options The options to pass to the compiler */ -export function createProgram(files: string[], options: ts.CompilerOptions) { +export function createTSProgram(files: string[], options: ts.CompilerOptions) { return ts.createProgram(files, options); } @@ -92,7 +92,7 @@ export function parseFromProgram( const checker = program.getTypeChecker(); const sourceFile = program.getSourceFile(filePath); - const programNode = t.programNode(); + const programNode = t.createProgram(); const reactImports: string[] = []; function visitImports(node: ts.Node) { @@ -166,11 +166,11 @@ export function parseFromProgram( return comment !== '' ? comment : undefined; } - function checkType(type: ts.Type, typeStack: number[], name: string): t.Node { + function checkType(type: ts.Type, typeStack: number[], name: string): t.PropType { // If the typeStack contains type.id we're dealing with an object that references itself. - // To prevent getting stuck in an infinite loop we just set it to an objectNode + // To prevent getting stuck in an infinite loop we just set it to an createObjectType if (typeStack.includes((type as any).id)) { - return t.objectNode(); + return t.createObjectType(); } { @@ -181,20 +181,20 @@ export function parseFromProgram( switch (typeName) { case 'global.JSX.Element': case 'React.ReactElement': { - return t.elementNode('element'); + return t.createElementType('element'); } case 'React.ElementType': { - return t.elementNode('elementType'); + return t.createElementType('elementType'); } case 'React.ReactNode': { - return t.unionNode([t.elementNode('node'), t.undefinedNode()]); + return t.createUnionType([t.createElementType('node'), t.createUndefinedType()]); } case 'React.Component': { - return t.instanceOfNode(typeName); + return t.createInstanceOfType(typeName); } case 'Element': case 'HTMLElement': { - return t.DOMElementNode(); + return t.createDOMElementType(); } default: // continue with function execution @@ -206,47 +206,47 @@ export function parseFromProgram( if (checker.isArrayType(type)) { // @ts-ignore const arrayType: ts.Type = checker.getElementTypeOfArrayType(type); - return t.arrayNode(checkType(arrayType, typeStack, name)); + return t.createArrayType(checkType(arrayType, typeStack, name)); } if (type.isUnion()) { - const node = t.unionNode(type.types.map((x) => checkType(x, typeStack, name))); + const node = t.createUnionType(type.types.map((x) => checkType(x, typeStack, name))); return node.types.length === 1 ? node.types[0] : node; } if (type.flags & ts.TypeFlags.String) { - return t.stringNode(); + return t.createStringType(); } if (type.flags & ts.TypeFlags.Number) { - return t.numericNode(); + return t.createNumericType(); } if (type.flags & ts.TypeFlags.Undefined) { - return t.undefinedNode(); + return t.createUndefinedType(); } if (type.flags & ts.TypeFlags.Any || type.flags & ts.TypeFlags.Unknown) { - return t.anyNode(); + return t.createAnyType(); } if (type.flags & ts.TypeFlags.Literal) { if (type.isLiteral()) { - return t.literalNode( + return t.createLiteralType( type.isStringLiteral() ? `"${type.value}"` : type.value, getDocumentation(type.symbol), ); } - return t.literalNode(checker.typeToString(type)); + return t.createLiteralType(checker.typeToString(type)); } if (type.flags & ts.TypeFlags.Null) { - return t.literalNode('null'); + return t.createLiteralType('null'); } if (type.getCallSignatures().length) { - return t.functionNode(); + return t.createFunctionType(); } // Object-like type @@ -260,14 +260,18 @@ export function parseFromProgram( shouldInclude({ name: symbol.getName(), depth: typeStack.length + 1 }), ); if (filtered.length > 0) { - return t.interfaceNode( - // eslint-disable-next-line @typescript-eslint/no-use-before-define -- TODO dependency cycle between checkSymbol and checkType - filtered.map((x) => checkSymbol(x, [...typeStack, (type as any).id])), + return t.createInterfaceType( + filtered.map((x) => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define -- TODO dependency cycle between checkSymbol and checkType + const definition = checkSymbol(x, [...typeStack, (type as any).id]); + + return definition.propType; + }), ); } } - return t.objectNode(); + return t.createObjectType(); } } @@ -276,13 +280,13 @@ export function parseFromProgram( type.flags & ts.TypeFlags.Object || (type.flags & ts.TypeFlags.NonPrimitive && checker.typeToString(type) === 'object') ) { - return t.objectNode(); + return t.createObjectType(); } console.warn( `Unable to handle node of type "ts.TypeFlags.${ts.TypeFlags[type.flags]}", using any`, ); - return t.anyNode(); + return t.createAnyType(); } function getSymbolFileNames(symbol: ts.Symbol): Set { @@ -291,7 +295,7 @@ export function parseFromProgram( return new Set(declarations.map((declaration) => declaration.getSourceFile().fileName)); } - function checkSymbol(symbol: ts.Symbol, typeStack: number[]): t.PropTypeNode { + function checkSymbol(symbol: ts.Symbol, typeStack: number[]): t.PropTypeDefinition { const declarations = symbol.getDeclarations(); const declaration = declarations && declarations[0]; @@ -314,14 +318,16 @@ export function parseFromProgram( name === 'React.ComponentType' || name === 'React.ReactElement' ) { - const elementNode = t.elementNode( + const elementNode = t.createElementType( name === 'React.ReactElement' ? 'element' : 'elementType', ); - return t.propTypeNode( + return t.createPropTypeDefinition( symbol.getName(), getDocumentation(symbol), - declaration.questionToken ? t.unionNode([t.undefinedNode(), elementNode]) : elementNode, + declaration.questionToken + ? t.createUnionType([t.createUndefinedType(), elementNode]) + : elementNode, symbolFilenames, createPropTypeId(symbol), ); @@ -351,20 +357,20 @@ export function parseFromProgram( // Typechecker only gives the type "any" if it's present in a union // This means the type of "a" in {a?:any} isn't "any | undefined" // So instead we check for the questionmark to detect optional types - let parsedType: t.Node | undefined; + let parsedType: t.PropType | undefined; if ( (type.flags & ts.TypeFlags.Any || type.flags & ts.TypeFlags.Unknown) && declaration && ts.isPropertySignature(declaration) ) { parsedType = declaration.questionToken - ? t.unionNode([t.undefinedNode(), t.anyNode()]) - : t.anyNode(); + ? t.createUnionType([t.createUndefinedType(), t.createAnyType()]) + : t.createAnyType(); } else { parsedType = checkType(type, typeStack, symbol.getName()); } - return t.propTypeNode( + return t.createPropTypeDefinition( symbol.getName(), getDocumentation(symbol), parsedType, @@ -384,7 +390,7 @@ export function parseFromProgram( const propsFilename = propsSourceFile !== undefined ? propsSourceFile.fileName : undefined; programNode.body.push( - t.componentNode( + t.createComponent( name, properties.map((x) => checkSymbol(x, [(type as any).id])), propsFilename, @@ -421,7 +427,7 @@ export function parseFromProgram( // { variant: 'a', href: string } & { variant: 'b' } // to // { variant: 'a' | 'b', href?: string } - const props: Record = {}; + const props: Record = {}; const usedPropsPerSignature: Set[] = []; programNode.body = programNode.body.filter((componentNode) => { if (componentNode.name === componentName) { @@ -434,10 +440,10 @@ export function parseFromProgram( if (currentTypeNode === undefined) { currentTypeNode = typeNode; } else if (currentTypeNode.$$id !== typeNode.$$id) { - currentTypeNode = t.propTypeNode( + currentTypeNode = t.createPropTypeDefinition( currentTypeNode.name, currentTypeNode.jsDoc, - t.unionNode([currentTypeNode.propType, typeNode.propType]), + t.createUnionType([currentTypeNode.propType, typeNode.propType]), new Set(Array.from(currentTypeNode.filenames).concat(Array.from(typeNode.filenames))), undefined, ); @@ -455,7 +461,7 @@ export function parseFromProgram( }); programNode.body.push( - t.componentNode( + t.createComponent( componentName, Object.entries(props).map(([name, propType]) => { const onlyUsedInSomeSignatures = usedPropsPerSignature.some( @@ -465,7 +471,7 @@ export function parseFromProgram( // mark as optional return { ...propType, - propType: t.unionNode([propType.propType, t.undefinedNode()]), + propType: t.createUnionType([propType.propType, t.createUndefinedType()]), }; } return propType; diff --git a/packages/typescript-to-proptypes/src/types/component.ts b/packages/typescript-to-proptypes/src/types/component.ts new file mode 100644 index 00000000000000..72040a8959cd98 --- /dev/null +++ b/packages/typescript-to-proptypes/src/types/component.ts @@ -0,0 +1,19 @@ +import { PropTypeDefinition } from './propTypeDefinition'; + +export interface Component { + name: string; + propsFilename?: string; + types: PropTypeDefinition[]; +} + +export function createComponent( + name: string, + types: PropTypeDefinition[], + propsFilename: string | undefined, +): Component { + return { + name, + types, + propsFilename, + }; +} diff --git a/packages/typescript-to-proptypes/src/types/index.ts b/packages/typescript-to-proptypes/src/types/index.ts index 5083bf29b44e3b..13cff5f8dd61aa 100644 --- a/packages/typescript-to-proptypes/src/types/index.ts +++ b/packages/typescript-to-proptypes/src/types/index.ts @@ -1,18 +1,5 @@ -export * from './nodes/baseNodes'; -export * from './nodes/program'; -export * from './nodes/component'; +export * from './propTypeDefinition'; +export * from './program'; +export * from './component'; -export * from './props/function'; -export * from './props/interface'; -export * from './props/string'; -export * from './props/union'; -export * from './props/undefined'; -export * from './props/boolean'; -export * from './props/numeric'; -export * from './props/literal'; -export * from './props/any'; -export * from './props/object'; -export * from './props/array'; -export * from './props/element'; -export * from './props/instanceOf'; -export * from './props/DOMElement'; +export * from './props'; diff --git a/packages/typescript-to-proptypes/src/types/nodes/baseNodes.ts b/packages/typescript-to-proptypes/src/types/nodes/baseNodes.ts deleted file mode 100644 index 74e00ded303b65..00000000000000 --- a/packages/typescript-to-proptypes/src/types/nodes/baseNodes.ts +++ /dev/null @@ -1,41 +0,0 @@ -export interface Node { - type: string; -} - -const propTypeTypeString = 'PropTypeNode'; - -export interface PropTypeNode extends Node { - name: string; - jsDoc?: string; - propType: Node; - filenames: Set; - /** - * @internal - */ - $$id: number | undefined; -} - -export function propTypeNode( - name: string, - jsDoc: string | undefined, - propType: Node, - filenames: Set, - id: number | undefined, -): PropTypeNode { - return { - type: propTypeTypeString, - name, - jsDoc, - propType, - filenames, - $$id: id, - }; -} - -export function isPropTypeNode(node: Node): node is PropTypeNode { - return node.type === propTypeTypeString; -} - -export interface DefinitionHolder extends Node { - types: PropTypeNode[]; -} diff --git a/packages/typescript-to-proptypes/src/types/nodes/component.ts b/packages/typescript-to-proptypes/src/types/nodes/component.ts deleted file mode 100644 index 7030b2db3d966c..00000000000000 --- a/packages/typescript-to-proptypes/src/types/nodes/component.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Node, DefinitionHolder, PropTypeNode } from './baseNodes'; - -const typeString = 'ComponentNode'; - -export interface ComponentNode extends DefinitionHolder { - name: string; - propsFilename?: string; -} - -export function componentNode( - name: string, - types: PropTypeNode[], - propsFilename: string | undefined, -): ComponentNode { - return { - type: typeString, - name, - types: types || [], - propsFilename, - }; -} - -export function isComponentNode(node: Node): node is ComponentNode { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/nodes/program.ts b/packages/typescript-to-proptypes/src/types/nodes/program.ts deleted file mode 100644 index 599141b36a3431..00000000000000 --- a/packages/typescript-to-proptypes/src/types/nodes/program.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Node } from './baseNodes'; -import { ComponentNode } from './component'; - -const typeString = 'ProgramNode'; - -export interface ProgramNode extends Node { - body: ComponentNode[]; -} - -export function programNode(body?: ComponentNode[]): ProgramNode { - return { - type: typeString, - body: body || [], - }; -} - -export function isProgramNode(node: Node): node is ProgramNode { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/program.ts b/packages/typescript-to-proptypes/src/types/program.ts new file mode 100644 index 00000000000000..b9dedded2393e0 --- /dev/null +++ b/packages/typescript-to-proptypes/src/types/program.ts @@ -0,0 +1,11 @@ +import { Component } from './component'; + +export interface Program { + body: Component[]; +} + +export function createProgram(body: Component[] = []): Program { + return { + body, + }; +} diff --git a/packages/typescript-to-proptypes/src/types/propTypeDefinition.ts b/packages/typescript-to-proptypes/src/types/propTypeDefinition.ts new file mode 100644 index 00000000000000..3f4fecad109b20 --- /dev/null +++ b/packages/typescript-to-proptypes/src/types/propTypeDefinition.ts @@ -0,0 +1,28 @@ +import { PropType } from './props'; + +export interface PropTypeDefinition { + name: string; + jsDoc?: string; + propType: PropType; + filenames: Set; + /** + * @internal + */ + $$id: number | undefined; +} + +export function createPropTypeDefinition( + name: string, + jsDoc: string | undefined, + propType: PropType, + filenames: Set, + id: number | undefined, +): PropTypeDefinition { + return { + name, + jsDoc, + propType, + filenames, + $$id: id, + }; +} diff --git a/packages/typescript-to-proptypes/src/types/props.ts b/packages/typescript-to-proptypes/src/types/props.ts new file mode 100644 index 00000000000000..904f53a29768f7 --- /dev/null +++ b/packages/typescript-to-proptypes/src/types/props.ts @@ -0,0 +1,204 @@ +import _ from 'lodash'; + +export type PropType = + | AnyType + | ArrayType + | BooleanType + | DOMElementType + | ElementType + | FunctionType + | InstanceOfType + | InterfaceType + | LiteralType + | NumericType + | ObjectType + | StringType + | UndefinedType + | UnionType; + +export interface AnyType { + type: 'any'; +} + +export function createAnyType(): AnyType { + return { + type: 'any', + }; +} + +export interface ArrayType { + arrayType: PropType; + type: 'array'; +} + +export function createArrayType(arrayType: PropType): ArrayType { + return { + type: 'array', + arrayType, + }; +} + +export interface BooleanType { + type: 'boolean'; +} + +export function createBooleanType(): BooleanType { + return { + type: 'boolean', + }; +} + +export interface DOMElementType { + optional?: boolean; + type: 'DOMElementNode'; +} + +export function createDOMElementType(optional?: boolean): DOMElementType { + return { + type: 'DOMElementNode', + optional, + }; +} + +export interface ElementType { + elementType: 'element' | 'node' | 'elementType'; + type: 'ElementNode'; +} + +export function createElementType(elementType: ElementType['elementType']): ElementType { + return { + type: 'ElementNode', + elementType, + }; +} + +export interface FunctionType { + type: 'FunctionNode'; +} + +export function createFunctionType(): FunctionType { + return { + type: 'FunctionNode', + }; +} + +export interface InstanceOfType { + instance: string; + type: 'InstanceOfNode'; +} + +export function createInstanceOfType(instance: string): InstanceOfType { + return { + type: 'InstanceOfNode', + instance, + }; +} + +export interface InterfaceType { + type: 'InterfaceNode'; + types: PropType[]; +} + +export function createInterfaceType(types: PropType[] = []): InterfaceType { + return { + type: 'InterfaceNode', + types, + }; +} + +export interface LiteralType { + value: unknown; + jsDoc?: string; + type: 'LiteralNode'; +} + +export function createLiteralType(value: unknown, jsDoc?: string): LiteralType { + return { + type: 'LiteralNode', + value, + jsDoc, + }; +} + +export interface NumericType { + type: 'NumericNode'; +} + +export function createNumericType(): NumericType { + return { + type: 'NumericNode', + }; +} + +export interface ObjectType { + type: 'ObjectNode'; +} + +export function createObjectType(): ObjectType { + return { + type: 'ObjectNode', + }; +} + +export interface StringType { + type: 'StringNode'; +} + +export function createStringType(): StringType { + return { + type: 'StringNode', + }; +} + +export interface UndefinedType { + type: 'UndefinedNode'; +} + +export function createUndefinedType(): UndefinedType { + return { + type: 'UndefinedNode', + }; +} + +export interface UnionType { + type: 'UnionNode'; + types: PropType[]; +} + +export function uniqueUnionTypes(node: UnionType): UnionType { + return { + type: node.type, + types: _.uniqBy(node.types, (type) => { + if (type.type === 'LiteralNode') { + return type.value; + } + + if (type.type === 'InstanceOfNode') { + return `${type.type}.${type.instance}`; + } + + return type.type; + }), + }; +} + +export function createUnionType(types: PropType[]): UnionType { + const flatTypes: PropType[] = []; + + function flattenTypes(nodes: PropType[]) { + nodes.forEach((type) => { + if (type.type === 'UnionNode') { + flattenTypes(type.types); + } else { + flatTypes.push(type); + } + }); + } + + flattenTypes(types); + + return uniqueUnionTypes({ + type: 'UnionNode', + types: flatTypes, + }); +} diff --git a/packages/typescript-to-proptypes/src/types/props/DOMElement.ts b/packages/typescript-to-proptypes/src/types/props/DOMElement.ts deleted file mode 100644 index 94c18a95e0d161..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/DOMElement.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'DOMElementNode'; - -interface DOMElementNode extends Node { - optional?: boolean; -} - -export function DOMElementNode(optional?: boolean): DOMElementNode { - return { - type: typeString, - optional, - }; -} - -export function isDOMElementNode(node: Node): node is DOMElementNode { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/any.ts b/packages/typescript-to-proptypes/src/types/props/any.ts deleted file mode 100644 index 53d5ba1f767bda..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/any.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'AnyNode'; - -export function anyNode(): Node { - return { - type: typeString, - }; -} - -export function isAnyNode(node: Node) { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/array.ts b/packages/typescript-to-proptypes/src/types/props/array.ts deleted file mode 100644 index f4396c522b964d..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/array.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'ArrayNode'; - -export interface ArrayNode extends Node { - arrayType: Node; -} - -export function arrayNode(arrayType: Node): ArrayNode { - return { - type: typeString, - arrayType, - }; -} - -export function isArrayNode(node: Node): node is ArrayNode { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/boolean.ts b/packages/typescript-to-proptypes/src/types/props/boolean.ts deleted file mode 100644 index d3cb6c5f181812..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/boolean.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'BooleanNode'; - -export function booleanNode(): Node { - return { - type: typeString, - }; -} - -export function isBooleanNode(node: Node) { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/element.ts b/packages/typescript-to-proptypes/src/types/props/element.ts deleted file mode 100644 index f5d8facd987be9..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/element.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'ElementNode'; -type ElementType = 'element' | 'node' | 'elementType'; - -interface ElementNode extends Node { - elementType: ElementType; -} - -export function elementNode(elementType: ElementType): ElementNode { - return { - type: typeString, - elementType, - }; -} - -export function isElementNode(node: Node): node is ElementNode { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/function.ts b/packages/typescript-to-proptypes/src/types/props/function.ts deleted file mode 100644 index c4dd374e1af42e..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/function.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'FunctionNode'; - -export function functionNode(): Node { - return { - type: typeString, - }; -} - -export function isFunctionNode(node: Node) { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/instanceOf.ts b/packages/typescript-to-proptypes/src/types/props/instanceOf.ts deleted file mode 100644 index 6de0b19c878850..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/instanceOf.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'InstanceOfNode'; - -export interface InstanceOfNode extends Node { - instance: string; -} - -export function instanceOfNode(instance: string): InstanceOfNode { - return { - type: typeString, - instance, - }; -} - -export function isInstanceOfNode(node: Node): node is InstanceOfNode { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/interface.ts b/packages/typescript-to-proptypes/src/types/props/interface.ts deleted file mode 100644 index 290ee3b1d659e2..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/interface.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Node, DefinitionHolder, PropTypeNode } from '../nodes/baseNodes'; - -const typeString = 'InterfaceNode'; - -export interface InterfaceNode extends DefinitionHolder {} - -export function interfaceNode(types?: PropTypeNode[]): InterfaceNode { - return { - type: typeString, - types: types || [], - }; -} - -export function isInterfaceNode(node: Node): node is InterfaceNode { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/literal.ts b/packages/typescript-to-proptypes/src/types/props/literal.ts deleted file mode 100644 index 61bf211ec166b5..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/literal.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'LiteralNode'; - -export interface LiteralNode extends Node { - value: unknown; - jsDoc?: string; -} - -export function literalNode(value: unknown, jsDoc?: string): LiteralNode { - return { - type: typeString, - value, - jsDoc, - }; -} - -export function isLiteralNode(node: Node): node is LiteralNode { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/numeric.ts b/packages/typescript-to-proptypes/src/types/props/numeric.ts deleted file mode 100644 index ad4d5598d64574..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/numeric.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'NumericNode'; - -export function numericNode(): Node { - return { - type: typeString, - }; -} - -export function isNumericNode(node: Node) { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/object.ts b/packages/typescript-to-proptypes/src/types/props/object.ts deleted file mode 100644 index 2404fcfbe506ff..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/object.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'ObjectNode'; - -export function objectNode(): Node { - return { - type: typeString, - }; -} - -export function isObjectNode(node: Node) { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/string.ts b/packages/typescript-to-proptypes/src/types/props/string.ts deleted file mode 100644 index 3ee7f7278c5452..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/string.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'StringNode'; - -export function stringNode(): Node { - return { - type: typeString, - }; -} - -export function isStringNode(node: Node) { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/undefined.ts b/packages/typescript-to-proptypes/src/types/props/undefined.ts deleted file mode 100644 index 7252f38fdbbd75..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/undefined.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Node } from '../nodes/baseNodes'; - -const typeString = 'UndefinedNode'; - -export function undefinedNode(): Node { - return { - type: typeString, - }; -} - -export function isUndefinedNode(node: Node) { - return node.type === typeString; -} diff --git a/packages/typescript-to-proptypes/src/types/props/union.ts b/packages/typescript-to-proptypes/src/types/props/union.ts deleted file mode 100644 index ea261e6e4e2b01..00000000000000 --- a/packages/typescript-to-proptypes/src/types/props/union.ts +++ /dev/null @@ -1,52 +0,0 @@ -import _ from 'lodash'; -import { isInstanceOfNode } from './instanceOf'; -import { isLiteralNode } from './literal'; -import { Node } from '../nodes/baseNodes'; - -const typeString = 'UnionNode'; - -export interface UnionNode extends Node { - types: Node[]; -} - -export function isUnionNode(node: Node): node is UnionNode { - return node.type === typeString; -} - -export function uniqueUnionTypes(node: UnionNode): UnionNode { - return { - type: node.type, - types: _.uniqBy(node.types, (x) => { - if (isLiteralNode(x)) { - return x.value; - } - - if (isInstanceOfNode(x)) { - return `${x.type}.${x.instance}`; - } - - return x.type; - }), - }; -} - -export function unionNode(types: Node[]): UnionNode { - const flatTypes: Node[] = []; - - function flattenTypes(nodes: Node[]) { - nodes.forEach((x) => { - if (isUnionNode(x)) { - flattenTypes(x.types); - } else { - flatTypes.push(x); - } - }); - } - - flattenTypes(types); - - return uniqueUnionTypes({ - type: typeString, - types: flatTypes, - }); -} diff --git a/packages/typescript-to-proptypes/test/index.test.ts b/packages/typescript-to-proptypes/test/index.test.ts index 8965362cef16d9..f042c826c69191 100644 --- a/packages/typescript-to-proptypes/test/index.test.ts +++ b/packages/typescript-to-proptypes/test/index.test.ts @@ -11,7 +11,7 @@ const prettierConfig = prettier.resolveConfig.sync(path.join(__dirname, '../.pre const testCases = glob.sync('**/input.{d.ts,ts,tsx}', { absolute: true, cwd: __dirname }); // Create program for all files to speed up tests -const program = ttp.createProgram( +const program = ttp.createTSProgram( testCases, ttp.loadConfig(path.resolve(__dirname, '../tsconfig.json')), ); @@ -49,7 +49,11 @@ for (const testCase of testCases) { let result = ''; // For d.ts files we just generate the AST if (!inputSource) { - result = ttp.generate(ast, options.generator); + result = ast.body + .map((component) => { + return ttp.generate(component, options.generator); + }) + .join('\n'); } else { // For .tsx? files we transpile them and inject the proptypesu const injected = ttp.inject(ast, inputSource, options.injector); diff --git a/packages/typescript-to-proptypes/test/sort-unions/options.ts b/packages/typescript-to-proptypes/test/sort-unions/options.ts index 7cada9e994cd86..ce6e1d39fcb3c8 100644 --- a/packages/typescript-to-proptypes/test/sort-unions/options.ts +++ b/packages/typescript-to-proptypes/test/sort-unions/options.ts @@ -2,8 +2,8 @@ import { TestOptions } from '../types'; const options: TestOptions = { injector: { - getSortLiteralUnions: (component, node) => { - if (component.name === 'Hidden' && node.name === 'only') { + getSortLiteralUnions: (component, propTypeDefinition) => { + if (component.name === 'Hidden' && propTypeDefinition.name === 'only') { return (a, b) => { // descending here to check that we actually change the order of the typings // It's unclear why TypeScript changes order of union members sometimes so we need to be sure diff --git a/scripts/generateProptypes.ts b/scripts/generateProptypes.ts index 0701adfaa4b7cd..42f1267479a3a1 100644 --- a/scripts/generateProptypes.ts +++ b/scripts/generateProptypes.ts @@ -126,7 +126,7 @@ const ignoreExternalDocumentation: Record = { Zoom: transitionCallbacks, }; -function sortBreakpointsLiteralByViewportAscending(a: ttp.LiteralNode, b: ttp.LiteralNode) { +function sortBreakpointsLiteralByViewportAscending(a: ttp.LiteralType, b: ttp.LiteralType) { // default breakpoints ordered by their size ascending const breakpointOrder: unknown[] = ['"xs"', '"sm"', '"md"', '"lg"', '"xl"']; @@ -298,7 +298,7 @@ async function run(argv: HandlerArgv) { .filter((filePath) => { return filePattern.test(filePath); }); - const program = ttp.createProgram(files, tsconfig); + const program = ttp.createTSProgram(files, tsconfig); const promises = files.map>(async (tsFile) => { const jsFile = tsFile.replace('.d.ts', '.js'); From 2b2863f8e80f7e5fa67fe6dc50211b7e083ab5ba Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 29 Jul 2020 09:18:34 +0200 Subject: [PATCH 2/6] improve debug --- packages/typescript-to-proptypes/src/generator.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/typescript-to-proptypes/src/generator.ts b/packages/typescript-to-proptypes/src/generator.ts index b9a1af154f0252..95f2025150b4e3 100644 --- a/packages/typescript-to-proptypes/src/generator.ts +++ b/packages/typescript-to-proptypes/src/generator.ts @@ -234,7 +234,9 @@ export function generate(component: t.Component, options: GenerateOptions = {}): .reduce((prev, curr) => `${prev},${curr}`)}])`; } - throw new Error(`Nothing to handle node of type "${propType.type}"`); + throw new Error( + `Nothing to handle node of type "${propType.type}" in "${context.propTypeDefinition.name}"`, + ); } function generatePropTypeDefinition( @@ -253,11 +255,13 @@ export function generate(component: t.Component, options: GenerateOptions = {}): (type) => type.type !== 'UndefinedNode' && !(type.type === 'LiteralNode' && type.value === 'null'), ); - if (propType.types.length === 1 && propType.types[0].type !== 'UndefinedNode') { + if (propType.types.length === 1 && propType.types[0].type !== 'LiteralNode') { propType = propType.types[0]; } } + console.log(isOptional, propTypeDefinition); + if (propType.type === 'DOMElementNode') { propType.optional = isOptional; // Handled internally in the validate function From 0f7f1267125b31b78d249fe5f974d50426ade92c Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 29 Jul 2020 10:01:27 +0200 Subject: [PATCH 3/6] fix required union members --- .../typescript-to-proptypes/src/generator.ts | 58 +++++++++---------- .../test/sort-unions/output.js | 2 +- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/packages/typescript-to-proptypes/src/generator.ts b/packages/typescript-to-proptypes/src/generator.ts index 95f2025150b4e3..4d3f750275ef91 100644 --- a/packages/typescript-to-proptypes/src/generator.ts +++ b/packages/typescript-to-proptypes/src/generator.ts @@ -182,8 +182,20 @@ export function generate(component: t.Component, options: GenerateOptions = {}): } if (propType.type === 'UnionNode') { + const uniqueTypes = t.uniqueUnionTypes(propType).types; + const isOptional = uniqueTypes.some((type) => type.type === 'UndefinedNode'); + const nonNullishUniqueTypes = uniqueTypes.filter((type) => { + return ( + type.type !== 'UndefinedNode' && !(type.type === 'LiteralNode' && type.value === 'null') + ); + }); + + if (uniqueTypes.length === 2 && uniqueTypes.some((type) => type.type === 'DOMElementNode')) { + return generatePropType(t.createDOMElementType(isOptional), context); + } + let [literals, rest] = _.partition( - t.uniqueUnionTypes(propType).types, + isOptional ? nonNullishUniqueTypes : uniqueTypes, (type): type is t.LiteralType => type.type === 'LiteralNode', ); @@ -220,18 +232,18 @@ export function generate(component: t.Component, options: GenerateOptions = {}): : ''; if (rest.length === 0) { - return literalProps; + return `${literalProps}${isOptional ? '' : '.isRequired'}`; } if (literals.length === 0 && rest.length === 1) { - return generatePropType(rest[0], context); + return `${generatePropType(rest[0], context)}${isOptional ? '' : '.isRequired'}`; } return `${importedName}.oneOfType([${ literalProps ? `${literalProps}, ` : '' }${rest .map((type) => generatePropType(type, context)) - .reduce((prev, curr) => `${prev},${curr}`)}])`; + .reduce((prev, curr) => `${prev},${curr}`)}])${isOptional ? '' : '.isRequired'}`; } throw new Error( @@ -243,37 +255,23 @@ export function generate(component: t.Component, options: GenerateOptions = {}): propTypeDefinition: t.PropTypeDefinition, context: { component: t.Component }, ): string { - let isOptional = false; - let propType = { ...propTypeDefinition.propType }; - - if ( - propType.type === 'UnionNode' && - propType.types.some((type) => type.type === 'UndefinedNode') - ) { - isOptional = true; - propType.types = propType.types.filter( - (type) => - type.type !== 'UndefinedNode' && !(type.type === 'LiteralNode' && type.value === 'null'), - ); - if (propType.types.length === 1 && propType.types[0].type !== 'LiteralNode') { - propType = propType.types[0]; - } - } - - console.log(isOptional, propTypeDefinition); - - if (propType.type === 'DOMElementNode') { - propType.optional = isOptional; - // Handled internally in the validate function - isOptional = true; + let isRequired: boolean | undefined = true; + + if (propTypeDefinition.propType.type === 'DOMElementNode') { + // DOMElement generator decides + isRequired = undefined; + } else if (propTypeDefinition.propType.type === 'UnionNode') { + // union generator decides + isRequired = undefined; } const validatorSource = reconcilePropTypes( propTypeDefinition, previousPropTypesSource.get(propTypeDefinition.name), - `${generatePropType(propType, { component: context.component, propTypeDefinition })}${ - isOptional ? '' : '.isRequired' - }`, + `${generatePropType(propTypeDefinition.propType, { + component: context.component, + propTypeDefinition, + })}${isRequired === true ? '.isRequired' : ''}`, ); return `${jsDoc(propTypeDefinition)}"${propTypeDefinition.name}": ${validatorSource},`; diff --git a/packages/typescript-to-proptypes/test/sort-unions/output.js b/packages/typescript-to-proptypes/test/sort-unions/output.js index b72aee5c2e1162..3a9308fa55003c 100644 --- a/packages/typescript-to-proptypes/test/sort-unions/output.js +++ b/packages/typescript-to-proptypes/test/sort-unions/output.js @@ -17,7 +17,7 @@ Hidden.propTypes = { */ only: PropTypes.oneOfType([ PropTypes.oneOf(['xl', 'md', 'xs']), - PropTypes.arrayOf(PropTypes.oneOf(['xl', 'md', 'xs'])), + PropTypes.arrayOf(PropTypes.oneOf(['xl', 'md', 'xs']).isRequired), ]), }; From 667159eb0861e7c54a9564d13b985bf32fa7bc7f Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 29 Jul 2020 10:26:09 +0200 Subject: [PATCH 4/6] fix optionality and key sorting in shapes --- packages/typescript-to-proptypes/src/generator.ts | 11 +++++++++-- packages/typescript-to-proptypes/src/parser.ts | 2 +- packages/typescript-to-proptypes/src/types/props.ts | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/typescript-to-proptypes/src/generator.ts b/packages/typescript-to-proptypes/src/generator.ts index 4d3f750275ef91..aebeb46234491b 100644 --- a/packages/typescript-to-proptypes/src/generator.ts +++ b/packages/typescript-to-proptypes/src/generator.ts @@ -119,8 +119,15 @@ export function generate(component: t.Component, options: GenerateOptions = {}): ): string { if (propType.type === 'InterfaceNode') { return `${importedName}.shape({\n${propType.types - .map((type) => generatePropType(type, context)) - .join('\n')}\n})`; + .slice() + .sort((a, b) => a[0].localeCompare(b[0])) + .map( + ([name, type]) => + `"${name}": ${generatePropType(type, context)}${ + type.type !== 'UnionNode' && type.type !== 'DOMElementNode' ? '.isRequired' : '' + }`, + ) + .join(',\n')}\n})`; } if (propType.type === 'FunctionNode') { diff --git a/packages/typescript-to-proptypes/src/parser.ts b/packages/typescript-to-proptypes/src/parser.ts index 7b017bff6a2cd9..540cdbaaa58e11 100644 --- a/packages/typescript-to-proptypes/src/parser.ts +++ b/packages/typescript-to-proptypes/src/parser.ts @@ -265,7 +265,7 @@ export function parseFromProgram( // eslint-disable-next-line @typescript-eslint/no-use-before-define -- TODO dependency cycle between checkSymbol and checkType const definition = checkSymbol(x, [...typeStack, (type as any).id]); - return definition.propType; + return [definition.name, definition.propType]; }), ); } diff --git a/packages/typescript-to-proptypes/src/types/props.ts b/packages/typescript-to-proptypes/src/types/props.ts index 904f53a29768f7..3c111b815d4fbf 100644 --- a/packages/typescript-to-proptypes/src/types/props.ts +++ b/packages/typescript-to-proptypes/src/types/props.ts @@ -96,10 +96,10 @@ export function createInstanceOfType(instance: string): InstanceOfType { export interface InterfaceType { type: 'InterfaceNode'; - types: PropType[]; + types: Array<[string, PropType]>; } -export function createInterfaceType(types: PropType[] = []): InterfaceType { +export function createInterfaceType(types: Array<[string, PropType]> = []): InterfaceType { return { type: 'InterfaceNode', types, From ab42f820b636b5d8166a6f9905bcb848033dd593 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 29 Jul 2020 10:26:37 +0200 Subject: [PATCH 5/6] Update propTypes --- packages/material-ui/src/Hidden/Hidden.js | 2 +- packages/material-ui/src/TablePagination/TablePagination.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/material-ui/src/Hidden/Hidden.js b/packages/material-ui/src/Hidden/Hidden.js index f529678f58929d..e187e882b638eb 100644 --- a/packages/material-ui/src/Hidden/Hidden.js +++ b/packages/material-ui/src/Hidden/Hidden.js @@ -104,7 +104,7 @@ Hidden.propTypes = { */ only: PropTypes.oneOfType([ PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']), - PropTypes.arrayOf(PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl'])), + PropTypes.arrayOf(PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']).isRequired), ]), /** * If `true`, screens this size and down will be hidden. diff --git a/packages/material-ui/src/TablePagination/TablePagination.js b/packages/material-ui/src/TablePagination/TablePagination.js index b1067309e9486d..2c718a07258c02 100644 --- a/packages/material-ui/src/TablePagination/TablePagination.js +++ b/packages/material-ui/src/TablePagination/TablePagination.js @@ -282,7 +282,7 @@ TablePagination.propTypes = { label: PropTypes.string.isRequired, value: PropTypes.number.isRequired, }), - ]), + ]).isRequired, ), /** * Props applied to the rows per page [`Select`](/api/select/) element. From 534687ea2061599b8428386a7697d7e3583e7bd7 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Thu, 30 Jul 2020 09:54:11 +0200 Subject: [PATCH 6/6] fix: createProgram -> createTSProgram --- docs/scripts/formattedTSDemos.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/scripts/formattedTSDemos.js b/docs/scripts/formattedTSDemos.js index 990f073297d433..d9559f1f50017b 100644 --- a/docs/scripts/formattedTSDemos.js +++ b/docs/scripts/formattedTSDemos.js @@ -114,7 +114,7 @@ async function main(argv) { const tsxFiles = await getFiles(path.join(workspaceRoot, 'docs/src/pages')); - const program = typescriptToProptypes.createProgram(tsxFiles, tsConfig); + const program = typescriptToProptypes.createTSProgram(tsxFiles, tsConfig); let successful = 0; let failed = 0;