diff --git a/src/generator.ts b/src/generator.ts index 3e970cb..1d42f2e 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -112,6 +112,12 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio } } + if (t.isDOMElementNode(propType)) { + propType.optional = isOptional; + // Handled internally in the validate function + isOptional = true; + } + return `${jsDoc(node)}"${node.name}": ${generate(propType, options)}${ isOptional ? '' : '.isRequired' },`; @@ -160,6 +166,20 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio return `${importedName}.instanceOf(${node.instance})`; } + if (t.isDOMElementNode(node)) { + return `function (props, propName) { + if (props[propName] == null) { + return ${ + node.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 (t.isArrayNode(node)) { if (t.isAnyNode(node.arrayType)) { return `${importedName}.array`; @@ -169,9 +189,7 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio } if (t.isUnionNode(node)) { - let [literals, rest] = _.partition(node.types, t.isLiteralNode); - literals = _.uniqBy(literals, (x) => x.value); - rest = _.uniqBy(rest, (x) => (t.isInstanceOfNode(x) ? `${x.type}.${x.instance}` : x.type)); + let [literals, rest] = _.partition(t.uniqueUnionTypes(node).types, t.isLiteralNode); literals = literals.sort((a, b) => a.value.localeCompare(b.value)); diff --git a/src/parser.ts b/src/parser.ts index 3706395..afe22c2 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -359,9 +359,9 @@ export function parseFromProgram( case 'React.Component': { return t.instanceOfNode(typeName); } - case 'Element': { - // Nextjs: Element isn't defined on the server - return t.instanceOfNode("typeof Element === 'undefined' ? Object : Element"); + case 'Element': + case 'HTMLElement': { + return t.DOMElementNode(); } } } @@ -374,7 +374,9 @@ export function parseFromProgram( } if (type.isUnion()) { - return t.unionNode(type.types.map((x) => checkType(x, typeStack, name))); + const node = t.unionNode(type.types.map((x) => checkType(x, typeStack, name))); + + return node.types.length === 1 ? node.types[0] : node; } if (type.flags & ts.TypeFlags.String) { diff --git a/src/types/index.ts b/src/types/index.ts index fffdd10..d382171 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -16,3 +16,4 @@ export * from './props/object'; export * from './props/array'; export * from './props/element'; export * from './props/instanceOf'; +export * from './props/DOMElement'; diff --git a/src/types/props/DOMElement.ts b/src/types/props/DOMElement.ts new file mode 100644 index 0000000..aa32a6d --- /dev/null +++ b/src/types/props/DOMElement.ts @@ -0,0 +1,18 @@ +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/src/types/props/union.ts b/src/types/props/union.ts index 9151299..f39cf2a 100644 --- a/src/types/props/union.ts +++ b/src/types/props/union.ts @@ -1,3 +1,5 @@ +import _ from 'lodash'; +import * as t from '../../types'; import { Node } from '../nodes/baseNodes'; const typeString = 'UnionNode'; @@ -21,12 +23,29 @@ export function unionNode(types: Node[]): UnionNode { }); } - return { + return uniqueUnionTypes({ type: typeString, types: flatTypes, - }; + }); } 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 (t.isLiteralNode(x)) { + return x.value; + } + + if (t.isInstanceOfNode(x)) { + return `${x.type}.${x.instance}`; + } + + return x.type; + }), + }; +} diff --git a/test/generator/html-elements/input.d.ts b/test/generator/html-elements/input.d.ts new file mode 100644 index 0000000..bead446 --- /dev/null +++ b/test/generator/html-elements/input.d.ts @@ -0,0 +1,6 @@ +export function Foo(props: { + element: Element; + optional?: Element; + htmlElement: HTMLElement; + bothTypes: Element | HTMLElement; +}): JSX.Element; diff --git a/test/generator/html-elements/output.js b/test/generator/html-elements/output.js new file mode 100644 index 0000000..2f83837 --- /dev/null +++ b/test/generator/html-elements/output.js @@ -0,0 +1,30 @@ +Foo.propTypes = { + bothTypes: function (props, propName) { + if (props[propName] == null) { + return 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"); + } + }, + element: function (props, propName) { + if (props[propName] == null) { + return 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"); + } + }, + htmlElement: function (props, propName) { + if (props[propName] == null) { + return 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"); + } + }, + optional: function (props, propName) { + if (props[propName] == null) { + return null; + } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) { + return new Error("Expected prop '" + propName + "' to be of type Element"); + } + }, +}; diff --git a/test/generator/html-elements/output.json b/test/generator/html-elements/output.json new file mode 100644 index 0000000..8f7ff71 --- /dev/null +++ b/test/generator/html-elements/output.json @@ -0,0 +1,22 @@ +{ + "type": "ProgramNode", + "body": [ + { + "type": "ComponentNode", + "name": "Foo", + "types": [ + { "type": "PropTypeNode", "name": "element", "propType": { "type": "DOMElementNode" } }, + { + "type": "PropTypeNode", + "name": "optional", + "propType": { + "type": "UnionNode", + "types": [{ "type": "UndefinedNode" }, { "type": "DOMElementNode" }] + } + }, + { "type": "PropTypeNode", "name": "htmlElement", "propType": { "type": "DOMElementNode" } }, + { "type": "PropTypeNode", "name": "bothTypes", "propType": { "type": "DOMElementNode" } } + ] + } + ] +}