Skip to content

Commit 7b0bff9

Browse files
authored
feat(injector): add reconcilePropTypes (#10)
1 parent dfcfe09 commit 7b0bff9

File tree

8 files changed

+154
-8
lines changed

8 files changed

+154
-8
lines changed

src/generator.ts

+26-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ export interface GenerateOptions {
2020
*/
2121
includeJSDoc?: boolean;
2222

23+
/**
24+
* Previous source code of the validator for each prop type
25+
*/
26+
previousPropTypesSource?: Map<string, string>;
27+
28+
/**
29+
* Given the `prop`, the `previous` source of the validator and the `generated` source:
30+
* What source should be injected? `previous` is `undefined` if the validator
31+
* didn't exist before
32+
* @default Uses `generated` source
33+
*/
34+
reconcilePropTypes?(
35+
proptype: t.PropTypeNode,
36+
previous: string | undefined,
37+
generated: string
38+
): string;
39+
2340
/**
2441
* Control which PropTypes are included in the final result
2542
* @param proptype The current PropType about to be converted to text
@@ -46,6 +63,8 @@ export function generate(node: t.Node | t.PropTypeNode[], options: GenerateOptio
4663
sortProptypes = true,
4764
importedName = 'PropTypes',
4865
includeJSDoc = true,
66+
previousPropTypesSource = new Map<string, string>(),
67+
reconcilePropTypes = (_prop: t.PropTypeNode, _previous: string, generated: string) => generated,
4968
shouldInclude,
5069
} = options;
5170

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

121-
return `${jsDoc(node)}"${node.name}": ${generate(propType, options)}${
122-
isOptional ? '' : '.isRequired'
123-
},`;
140+
const validatorSource = reconcilePropTypes(
141+
node,
142+
previousPropTypesSource.get(node.name),
143+
`${generate(propType, options)}${isOptional ? '' : '.isRequired'}`
144+
);
145+
146+
return `${jsDoc(node)}"${node.name}": ${validatorSource},`;
124147
}
125148

126149
if (t.isInterfaceNode(node)) {

src/injector.ts

+26-4
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export type InjectOptions = {
3131
* Options passed to babel.transformSync
3232
*/
3333
babelOptions?: babel.TransformOptions;
34-
} & Pick<GenerateOptions, 'sortProptypes' | 'includeJSDoc' | 'comment'>;
34+
} & Pick<GenerateOptions, 'sortProptypes' | 'includeJSDoc' | 'comment' | 'reconcilePropTypes'>;
3535

3636
/**
3737
* Injects the PropTypes from `parse` into the provided JavaScript code
@@ -84,8 +84,16 @@ function plugin(
8484
options: InjectOptions = {},
8585
mapOfPropTypes: Map<string, string>
8686
): babel.PluginObj {
87-
const { includeUnusedProps = false, removeExistingPropTypes = false, ...otherOptions } = options;
88-
87+
const {
88+
includeUnusedProps = false,
89+
reconcilePropTypes = (
90+
_prop: t.PropTypeNode,
91+
_previous: string | undefined,
92+
generated: string
93+
) => generated,
94+
removeExistingPropTypes = false,
95+
...otherOptions
96+
} = options;
8997
const shouldInclude: Exclude<InjectOptions['shouldInclude'], undefined> = (data) => {
9098
if (options.shouldInclude) {
9199
const result = options.shouldInclude(data);
@@ -101,11 +109,12 @@ function plugin(
101109
let needImport = false;
102110
let alreadyImported = false;
103111
let originalPropTypesPath: null | babel.NodePath = null;
112+
let previousPropTypesSource = new Map<string, string>();
104113

105114
return {
106115
visitor: {
107116
Program: {
108-
enter(path) {
117+
enter(path, state: any) {
109118
if (
110119
!path.node.body.some((n) => {
111120
if (
@@ -131,6 +140,17 @@ function plugin(
131140
babelTypes.isIdentifier(node.expression.left.property, { name: 'propTypes' })
132141
) {
133142
originalPropTypesPath = nodePath;
143+
144+
if (babelTypes.isObjectExpression(node.expression.right)) {
145+
const { code } = state.file;
146+
147+
node.expression.right.properties.forEach((property) => {
148+
if (babelTypes.isObjectProperty(property)) {
149+
const validatorSource = code.slice(property.value.start, property.value.end);
150+
previousPropTypesSource.set(property.key.name, validatorSource);
151+
}
152+
});
153+
}
134154
}
135155
});
136156
},
@@ -266,6 +286,8 @@ function plugin(
266286
const source = generate(props, {
267287
...otherOptions,
268288
importedName: importName,
289+
previousPropTypesSource,
290+
reconcilePropTypes,
269291
shouldInclude: (prop) => shouldInclude({ component: props, prop, usedProps }),
270292
});
271293

test/index.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ for (const testCase of testCases) {
5555
let inputSource = null;
5656
if (testCase.endsWith('.d.ts')) {
5757
try {
58-
inputSource = fs.readFileSync(inputJS, { encoding: 'utf8' });
58+
inputSource = fs.readFileSync(inputJS, 'utf8');
5959
} catch (error) {}
6060
} else {
6161
inputSource = ttp.ts.transpileModule(fs.readFileSync(testCase, 'utf8'), {

test/reconcile-prop-types/input.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as React from 'react';
2+
3+
interface Props {
4+
children: React.ReactNode;
5+
}
6+
7+
export default function Component(props: Props): JSX.Element;

test/reconcile-prop-types/input.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from 'react';
2+
import * as PropTypes from 'prop-types';
3+
import { chainPropTypes } from 'some-utils-module';
4+
5+
function Component(props) {
6+
const { children } = props;
7+
return (
8+
<button>
9+
<span>{children}</span>
10+
</button>
11+
);
12+
}
13+
14+
Component.propTypes = {
15+
children: chainPropTypes(PropTypes.node.isRequired, (props) => {
16+
const summary = React.Children.toArray(props.children)[0];
17+
if (isFragment(summary)) {
18+
return new Error('Not accepting Fragments');
19+
}
20+
21+
if (!React.isValidElement(summary)) {
22+
return new Error('First child must be an element');
23+
}
24+
25+
return null;
26+
}),
27+
};
28+
29+
export default Component;

test/reconcile-prop-types/options.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { TestOptions } from '../types';
2+
3+
const options: TestOptions = {
4+
injector: {
5+
removeExistingPropTypes: true,
6+
reconcilePropTypes: (prop, previous: any, generated) => {
7+
const isCustomValidator = previous !== undefined && !previous.startsWith('PropTypes');
8+
9+
if (isCustomValidator) {
10+
return previous;
11+
}
12+
return generated;
13+
},
14+
},
15+
};
16+
17+
export default options;

test/reconcile-prop-types/output.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from 'react';
2+
import * as PropTypes from 'prop-types';
3+
import { chainPropTypes } from 'some-utils-module';
4+
5+
function Component(props) {
6+
const { children } = props;
7+
return (
8+
<button>
9+
<span>{children}</span>
10+
</button>
11+
);
12+
}
13+
14+
Component.propTypes = {
15+
children: chainPropTypes(PropTypes.node.isRequired, (props) => {
16+
const summary = React.Children.toArray(props.children)[0];
17+
if (isFragment(summary)) {
18+
return new Error('Not accepting Fragments');
19+
}
20+
21+
if (!React.isValidElement(summary)) {
22+
return new Error('First child must be an element');
23+
}
24+
25+
return null;
26+
}),
27+
};
28+
29+
export default Component;

test/reconcile-prop-types/output.json

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"type": "ProgramNode",
3+
"body": [
4+
{
5+
"type": "ComponentNode",
6+
"name": "Component",
7+
"types": [
8+
{
9+
"type": "PropTypeNode",
10+
"name": "children",
11+
"propType": {
12+
"type": "UnionNode",
13+
"types": [{ "type": "ElementNode", "elementType": "node" }, { "type": "UndefinedNode" }]
14+
}
15+
}
16+
]
17+
}
18+
]
19+
}

0 commit comments

Comments
 (0)