packages/typescript-to-proptypes/src/generator.ts/nodeToStringName
export function generate(component: t.Component, options: GenerateOptions = {}): string {
  const {
    disableTypescriptPropTypesValidation = false,
    importedName = 'PropTypes',
    includeJSDoc = true,
    sortProptypes = true,
    previousPropTypesSource = new Map<string, string>(),
    reconcilePropTypes = (_prop: t.PropTypeDefinition, _previous: string, generated: string) =>
      generated,
    shouldInclude,
    getSortLiteralUnions = () => defaultSortLiteralUnions,
  } = options;

  function jsDoc(documentedNode: t.PropTypeDefinition | t.LiteralType): string {
    if (!includeJSDoc || !documentedNode.jsDoc) {
      return '';
    }
    return `/**\n* ${documentedNode.jsDoc
      .split(/\r?\n/)
      .reduce((prev, curr) => `${prev}\n* ${curr}`)}\n*/\n`;
  }

  function generatePropType(
    propType: t.PropType,
    context: { component: t.Component; propTypeDefinition: t.PropTypeDefinition },
  ): string {
    if (propType.type === 'InterfaceNode') {
      return `${importedName}.shape({\n${propType.types
        .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') {
      return `${importedName}.func`;
    }

    if (propType.type === 'StringNode') {
      return `${importedName}.string`;
    }

    if (propType.type === 'boolean') {
      return `${importedName}.bool`;
    }

    if (propType.type === 'NumericNode') {
      return `${importedName}.number`;
    }

    if (propType.type === 'LiteralNode') {
      return `${importedName}.oneOf([${jsDoc(propType)}${propType.value}])`;
    }

    if (propType.type === 'ObjectNode') {
      return `${importedName}.object`;
    }

    if (propType.type === 'any') {
      return `${importedName}.any`;
    }

    if (propType.type === 'ElementNode') {
      return `${importedName}.${propType.elementType}`;
    }

    if (propType.type === 'InstanceOfNode') {
      return `${importedName}.instanceOf(${propType.instance})`;
    }

    if (propType.type === 'DOMElementNode') {
      return `function (props, propName) {
			if (props[propName] == null) {
				return ${
          propType.optional
            ? 'null'
            : `new Error("Prop '" + propName + "' is required but wasn't specified")`
        }
			} else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) {
				return new Error("Expected prop '" + propName + "' to be of type Element")
			}
		}`;
    }

    if (propType.type === 'array') {
      if (propType.arrayType.type === 'any') {
        return `${importedName}.array`;
      }

      return `${importedName}.arrayOf(${generatePropType(propType.arrayType, context)})`;
    }

    if (propType.type === 'UnionNode') {
      const uniqueTypes = t.uniqueUnionTypes(propType).types;
      const isOptional = uniqueTypes.some(
        (type) =>
          type.type === 'UndefinedNode' || (type.type === 'LiteralNode' && type.value === 'null'),
      );
      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(
        isOptional ? nonNullishUniqueTypes : uniqueTypes,
        (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`;
        }

        return type.type;
      };

      rest = rest.sort((a, b) => nodeToStringName(a).localeCompare(nodeToStringName(b)));

      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');
      }

      const literalProps =
        literals.length !== 0
          ? `${importedName}.oneOf([${literals
              .map((x) => `${jsDoc(x)}${x.value}`)
              .reduce((prev, curr) => `${prev},${curr}`)}])`
          : '';

      if (rest.length === 0) {
        return `${literalProps}${isOptional ? '' : '.isRequired'}`;
      }

      if (literals.length === 0 && rest.length === 1) {
        return `${generatePropType(rest[0], context)}${isOptional ? '' : '.isRequired'}`;
      }

      return `${importedName}.oneOfType([${
        literalProps ? `${literalProps}, ` : ''
      }${rest
        .map((type) => generatePropType(type, context))
        .reduce((prev, curr) => `${prev},${curr}`)}])${isOptional ? '' : '.isRequired'}`;
    }

    throw new Error(
      `Nothing to handle node of type "${propType.type}" in "${context.propTypeDefinition.name}"`,
    );
  }

  function generatePropTypeDefinition(
    propTypeDefinition: t.PropTypeDefinition,
    context: { component: t.Component },
  ): string {
    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(propTypeDefinition.propType, {
        component: context.component,
        propTypeDefinition,
      })}${isRequired === true ? '.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));
  }

  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`;

  const componentNameNode = disableTypescriptPropTypesValidation
    ? `(${component.name} as any)`
    : component.name;

  return `${componentNameNode}.propTypes = {\n${
    comment !== undefined ? comment : ''
  }${generated}\n}`;
}