Skip to content

Commit

Permalink
feat: make literal sort configurable (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon committed Jun 19, 2020
1 parent 66fa209 commit 4bec240
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 19 deletions.
72 changes: 53 additions & 19 deletions packages/typescript-to-proptypes/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,46 @@ export interface GenerateOptions {
* }
*/
comment?: string;

/**
* Overrides the given `sortLiteralUnions` based on the proptype.
* 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;

/**
* 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;
}

function defaultSortLiteralUnions(a: t.LiteralNode, b: t.LiteralNode) {
const { value: valueA } = a;
const { value: valueB } = b;
// numbers ascending
if (typeof valueA === 'number' && typeof valueB === 'number') {
return valueA - valueB;
}
// numbers last
if (typeof valueA === 'number') {
return 1;
}
if (typeof valueB === 'number') {
return -1;
}
// sort anything else by their stringified value
return String(valueA).localeCompare(String(valueB));
}

/**
Expand All @@ -60,12 +100,15 @@ export interface GenerateOptions {
*/
export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptions = {}): string {
const {
component,
sortProptypes = true,
importedName = 'PropTypes',
includeJSDoc = true,
previousPropTypesSource = new Map<string, string>(),
reconcilePropTypes = (_prop: t.PropTypeNode, _previous: string, generated: string) => generated,
shouldInclude,
getSortLiteralUnions = () => defaultSortLiteralUnions,
sortLiteralUnions = defaultSortLiteralUnions,
} = options;

function jsDoc(node: t.PropTypeNode | t.LiteralNode) {
Expand Down Expand Up @@ -105,7 +148,7 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio
}

if (t.isComponentNode(node)) {
const generated = generate(node.types, options);
const generated = generate(node.types, { ...options, component: node });
if (generated.length === 0) {
return '';
}
Expand All @@ -117,6 +160,10 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio
return `${node.name}.propTypes = {\n${comment ? 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 };
Expand All @@ -140,7 +187,10 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio
const validatorSource = reconcilePropTypes(
node,
previousPropTypesSource.get(node.name),
`${generate(propType, options)}${isOptional ? '' : '.isRequired'}`,
`${generate(propType, {
...options,
sortLiteralUnions: getSortLiteralUnions(component, node) || sortLiteralUnions,
})}${isOptional ? '' : '.isRequired'}`,
);

return `${jsDoc(node)}"${node.name}": ${validatorSource},`;
Expand Down Expand Up @@ -214,23 +264,7 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio
if (t.isUnionNode(node)) {
let [literals, rest] = _.partition(t.uniqueUnionTypes(node).types, t.isLiteralNode);

literals = literals.sort((a, b) => {
const { value: valueA } = a;
const { value: valueB } = b;
// numbers ascending
if (typeof valueA === 'number' && typeof valueB === 'number') {
return valueA - valueB;
}
// numbers last
if (typeof valueA === 'number') {
return 1;
}
if (typeof valueB === 'number') {
return -1;
}
// sort anything else by their stringified value
return String(valueA).localeCompare(String(valueB));
});
literals = literals.sort(sortLiteralUnions);

const nodeToStringName = (obj: t.Node): string => {
if (t.isInstanceOfNode(obj)) {
Expand Down
15 changes: 15 additions & 0 deletions packages/typescript-to-proptypes/src/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ export type InjectOptions = {
usedProps: string[];
}): boolean | undefined;

/**
* You can override the order of literals in unions based on the proptype.
*
* By default literals in unions are sorted by:
* - numbers last, ascending
* - anything else by their stringified value using localeCompare
* Note: The order of the literals as they "appear" in the typings cannot be preserved.
* Sometimes the type checker preserves it, sometimes it doesn't.
* 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;

/**
* Options passed to babel.transformSync
*/
Expand Down
16 changes: 16 additions & 0 deletions packages/typescript-to-proptypes/test/sort-unions/input.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';

type Breakpoint = 'xs' | 'md' | 'xl';

export interface Props {
/**
* will be sorted alphanumeric
*/
color?: 'inherit' | 'default' | 'primary' | 'secondary';
/**
* will be sorted by viewport size descending
*/
only?: Breakpoint | Breakpoint[];
}

export default function Hidden(props: Props): JSX.Element;
7 changes: 7 additions & 0 deletions packages/typescript-to-proptypes/test/sort-unions/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';

export default function Hidden(props) {
const { color, only } = props;

return <div color={color} hidden={only !== 'xs'} />;
}
21 changes: 21 additions & 0 deletions packages/typescript-to-proptypes/test/sort-unions/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { TestOptions } from '../types';

const options: TestOptions = {
injector: {
getSortLiteralUnions: (component, node) => {
if (component.name === 'Hidden' && node.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
const breakpointOrder: unknown[] = ['"xl"', '"md"', '"xs"'];

return breakpointOrder.indexOf(a.value) - breakpointOrder.indexOf(b.value);
};
}
// default sort
return undefined;
},
},
};

export default options;
24 changes: 24 additions & 0 deletions packages/typescript-to-proptypes/test/sort-unions/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';
import PropTypes from 'prop-types';

function Hidden(props) {
const { color, only } = props;

return <div color={color} hidden={only !== 'xs'} />;
}

Hidden.propTypes = {
/**
* will be sorted alphanumeric
*/
color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']),
/**
* will be sorted by viewport size descending
*/
only: PropTypes.oneOfType([
PropTypes.oneOf(['xl', 'md', 'xs']),
PropTypes.arrayOf(PropTypes.oneOf(['xl', 'md', 'xs'])),
]),
};

export default Hidden;

0 comments on commit 4bec240

Please sign in to comment.