Skip to content

Commit

Permalink
feat(injector): add reconcilePropTypes (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon authored Apr 6, 2020
1 parent dfcfe09 commit 7b0bff9
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 8 deletions.
29 changes: 26 additions & 3 deletions src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,23 @@ export interface GenerateOptions {
*/
includeJSDoc?: boolean;

/**
* Previous source code of the validator for each prop type
*/
previousPropTypesSource?: Map<string, string>;

/**
* Given the `prop`, the `previous` source of the validator and the `generated` source:
* What source should be injected? `previous` is `undefined` if the validator
* didn't exist before
* @default Uses `generated` source
*/
reconcilePropTypes?(
proptype: t.PropTypeNode,
previous: string | undefined,
generated: string
): string;

/**
* Control which PropTypes are included in the final result
* @param proptype The current PropType about to be converted to text
Expand All @@ -46,6 +63,8 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio
sortProptypes = true,
importedName = 'PropTypes',
includeJSDoc = true,
previousPropTypesSource = new Map<string, string>(),
reconcilePropTypes = (_prop: t.PropTypeNode, _previous: string, generated: string) => generated,
shouldInclude,
} = options;

Expand Down Expand Up @@ -118,9 +137,13 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio
isOptional = true;
}

return `${jsDoc(node)}"${node.name}": ${generate(propType, options)}${
isOptional ? '' : '.isRequired'
},`;
const validatorSource = reconcilePropTypes(
node,
previousPropTypesSource.get(node.name),
`${generate(propType, options)}${isOptional ? '' : '.isRequired'}`
);

return `${jsDoc(node)}"${node.name}": ${validatorSource},`;
}

if (t.isInterfaceNode(node)) {
Expand Down
30 changes: 26 additions & 4 deletions src/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type InjectOptions = {
* Options passed to babel.transformSync
*/
babelOptions?: babel.TransformOptions;
} & Pick<GenerateOptions, 'sortProptypes' | 'includeJSDoc' | 'comment'>;
} & Pick<GenerateOptions, 'sortProptypes' | 'includeJSDoc' | 'comment' | 'reconcilePropTypes'>;

/**
* Injects the PropTypes from `parse` into the provided JavaScript code
Expand Down Expand Up @@ -84,8 +84,16 @@ function plugin(
options: InjectOptions = {},
mapOfPropTypes: Map<string, string>
): babel.PluginObj {
const { includeUnusedProps = false, removeExistingPropTypes = false, ...otherOptions } = options;

const {
includeUnusedProps = false,
reconcilePropTypes = (
_prop: t.PropTypeNode,
_previous: string | undefined,
generated: string
) => generated,
removeExistingPropTypes = false,
...otherOptions
} = options;
const shouldInclude: Exclude<InjectOptions['shouldInclude'], undefined> = (data) => {
if (options.shouldInclude) {
const result = options.shouldInclude(data);
Expand All @@ -101,11 +109,12 @@ function plugin(
let needImport = false;
let alreadyImported = false;
let originalPropTypesPath: null | babel.NodePath = null;
let previousPropTypesSource = new Map<string, string>();

return {
visitor: {
Program: {
enter(path) {
enter(path, state: any) {
if (
!path.node.body.some((n) => {
if (
Expand All @@ -131,6 +140,17 @@ function plugin(
babelTypes.isIdentifier(node.expression.left.property, { name: 'propTypes' })
) {
originalPropTypesPath = nodePath;

if (babelTypes.isObjectExpression(node.expression.right)) {
const { code } = state.file;

node.expression.right.properties.forEach((property) => {
if (babelTypes.isObjectProperty(property)) {
const validatorSource = code.slice(property.value.start, property.value.end);
previousPropTypesSource.set(property.key.name, validatorSource);
}
});
}
}
});
},
Expand Down Expand Up @@ -266,6 +286,8 @@ function plugin(
const source = generate(props, {
...otherOptions,
importedName: importName,
previousPropTypesSource,
reconcilePropTypes,
shouldInclude: (prop) => shouldInclude({ component: props, prop, usedProps }),
});

Expand Down
2 changes: 1 addition & 1 deletion test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ for (const testCase of testCases) {
let inputSource = null;
if (testCase.endsWith('.d.ts')) {
try {
inputSource = fs.readFileSync(inputJS, { encoding: 'utf8' });
inputSource = fs.readFileSync(inputJS, 'utf8');
} catch (error) {}
} else {
inputSource = ttp.ts.transpileModule(fs.readFileSync(testCase, 'utf8'), {
Expand Down
7 changes: 7 additions & 0 deletions test/reconcile-prop-types/input.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';

interface Props {
children: React.ReactNode;
}

export default function Component(props: Props): JSX.Element;
29 changes: 29 additions & 0 deletions test/reconcile-prop-types/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { chainPropTypes } from 'some-utils-module';

function Component(props) {
const { children } = props;
return (
<button>
<span>{children}</span>
</button>
);
}

Component.propTypes = {
children: chainPropTypes(PropTypes.node.isRequired, (props) => {
const summary = React.Children.toArray(props.children)[0];
if (isFragment(summary)) {
return new Error('Not accepting Fragments');
}

if (!React.isValidElement(summary)) {
return new Error('First child must be an element');
}

return null;
}),
};

export default Component;
17 changes: 17 additions & 0 deletions test/reconcile-prop-types/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { TestOptions } from '../types';

const options: TestOptions = {
injector: {
removeExistingPropTypes: true,
reconcilePropTypes: (prop, previous: any, generated) => {
const isCustomValidator = previous !== undefined && !previous.startsWith('PropTypes');

if (isCustomValidator) {
return previous;
}
return generated;
},
},
};

export default options;
29 changes: 29 additions & 0 deletions test/reconcile-prop-types/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { chainPropTypes } from 'some-utils-module';

function Component(props) {
const { children } = props;
return (
<button>
<span>{children}</span>
</button>
);
}

Component.propTypes = {
children: chainPropTypes(PropTypes.node.isRequired, (props) => {
const summary = React.Children.toArray(props.children)[0];
if (isFragment(summary)) {
return new Error('Not accepting Fragments');
}

if (!React.isValidElement(summary)) {
return new Error('First child must be an element');
}

return null;
}),
};

export default Component;
19 changes: 19 additions & 0 deletions test/reconcile-prop-types/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"type": "ProgramNode",
"body": [
{
"type": "ComponentNode",
"name": "Component",
"types": [
{
"type": "PropTypeNode",
"name": "children",
"propType": {
"type": "UnionNode",
"types": [{ "type": "ElementNode", "elementType": "node" }, { "type": "UndefinedNode" }]
}
}
]
}
]
}

0 comments on commit 7b0bff9

Please sign in to comment.