Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new: Add JSX Pragma and Preact support #42

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions src/convertBabelToPropTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { addComment } from '@babel/types';
import { convertSymbolFromSource } from './convertTSToPropTypes';
import extractEnumValues from './extractEnumValues';
import getTypeName from './getTypeName';
import {
getDefaultImportNameFromPragma,
getNodeTypesFromPragma,
getFunctionTypesFromPragma,
getElementTypesFromPragma,
} from './jsx-pragma';
import {
createCall,
createMember,
Expand All @@ -22,6 +28,8 @@ function convert(type: any, state: ConvertState, depth: number): PropType | null
const { reactImportedName, propTypes } = state;
const propTypesImportedName = propTypes.defaultImport;
const isMaxDepth = depth >= state.options.maxDepth;
const { jsxPragma } = state.options;
const defaultImportName = getDefaultImportNameFromPragma(jsxPragma);

// Remove wrapping parens
if (t.isTSParenthesizedType(type)) {
Expand Down Expand Up @@ -123,37 +131,33 @@ function convert(type: any, state: ConvertState, depth: number): PropType | null

// node
} else if (
isReactTypeMatch(name, 'ReactText', reactImportedName) ||
isReactTypeMatch(name, 'ReactNode', reactImportedName) ||
isReactTypeMatch(name, 'ReactType', reactImportedName) ||
isReactTypeMatch(name, 'ElementType', reactImportedName)
getNodeTypesFromPragma(jsxPragma).some((element) =>
isReactTypeMatch(name, element, defaultImportName, reactImportedName),
)
) {
return createMember(t.identifier('node'), propTypesImportedName);

// function
} else if (
isReactTypeMatch(name, 'ComponentType', reactImportedName) ||
isReactTypeMatch(name, 'ComponentClass', reactImportedName) ||
isReactTypeMatch(name, 'StatelessComponent', reactImportedName) ||
isReactTypeMatch(name, 'ElementType', reactImportedName)
getFunctionTypesFromPragma(jsxPragma).some((element) =>
isReactTypeMatch(name, element, defaultImportName, reactImportedName),
)
) {
return getInstalledPropTypesVersion() >= PROP_TYPES_15_7
? createMember(t.identifier('elementType'), propTypesImportedName)
: createMember(t.identifier('func'), propTypesImportedName);

// element
} else if (
isReactTypeMatch(name, 'Element', 'JSX') ||
isReactTypeMatch(name, 'ReactElement', reactImportedName) ||
isReactTypeMatch(name, 'ComponentElement', reactImportedName) ||
isReactTypeMatch(name, 'FunctionComponentElement', reactImportedName) ||
isReactTypeMatch(name, 'DOMElement', reactImportedName) ||
isReactTypeMatch(name, 'SFCElement', reactImportedName)
isReactTypeMatch(name, 'Element', defaultImportName, 'JSX') ||
getElementTypesFromPragma(jsxPragma).some((element) =>
isReactTypeMatch(name, element, defaultImportName, reactImportedName),
)
) {
return createMember(t.identifier('element'), propTypesImportedName);

// oneOfType
} else if (isReactTypeMatch(name, 'Ref', reactImportedName)) {
} else if (isReactTypeMatch(name, 'Ref', defaultImportName, reactImportedName)) {
return createCall(
t.identifier('oneOfType'),
[
Expand Down
33 changes: 19 additions & 14 deletions src/convertTSToPropTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,20 @@ import {
isReactTypeMatch,
// wrapIsRequired,
} from './propTypes';
import {
getDefaultImportNameFromPragma,
getNodeTypesFromPragma,
getFunctionTypesFromPragma,
getElementTypesFromPragma,
} from './jsx-pragma';
import { ConvertState, PropType } from './types';

export function convert(type: ts.Type, state: ConvertState, depth: number): PropType | null {
const { reactImportedName, propTypes } = state;
const propTypesImportedName = propTypes.defaultImport;
const isMaxDepth = depth >= (state.options.maxDepth || 3);
const jsxPragma = state.options.jsxPragma;
const defaultImportName = getDefaultImportNameFromPragma(jsxPragma);

// Remove wrapping parens
// if (ts.isParenthesizedExpression(type)) {
Expand Down Expand Up @@ -82,34 +90,31 @@ export function convert(type: ts.Type, state: ConvertState, depth: number): Prop

// node
if (
isReactTypeMatch(name, 'ReactText', reactImportedName) ||
isReactTypeMatch(name, 'ReactNode', reactImportedName) ||
isReactTypeMatch(name, 'ReactType', reactImportedName)
getNodeTypesFromPragma(jsxPragma).some((element) =>
isReactTypeMatch(name, element, defaultImportName, reactImportedName),
)
) {
return createMember(t.identifier('node'), propTypesImportedName);

// function
} else if (
isReactTypeMatch(name, 'ComponentType', reactImportedName) ||
isReactTypeMatch(name, 'ComponentClass', reactImportedName) ||
isReactTypeMatch(name, 'StatelessComponent', reactImportedName) ||
isReactTypeMatch(name, 'ElementType', reactImportedName)
getFunctionTypesFromPragma(jsxPragma).some((element) =>
isReactTypeMatch(name, element, defaultImportName, reactImportedName),
)
) {
return createMember(t.identifier('func'), propTypesImportedName);

// element
} else if (
isReactTypeMatch(name, 'Element', 'JSX') ||
isReactTypeMatch(name, 'ReactElement', reactImportedName) ||
isReactTypeMatch(name, 'ComponentElement', reactImportedName) ||
isReactTypeMatch(name, 'FunctionComponentElement', reactImportedName) ||
isReactTypeMatch(name, 'DOMElement', reactImportedName) ||
isReactTypeMatch(name, 'SFCElement', reactImportedName)
isReactTypeMatch(name, 'Element', defaultImportName, 'JSX') ||
getElementTypesFromPragma(jsxPragma).some((element) =>
isReactTypeMatch(name, element, defaultImportName, reactImportedName),
)
) {
return createMember(t.identifier('element'), propTypesImportedName);

// oneOfType
} else if (isReactTypeMatch(name, 'Ref', reactImportedName)) {
} else if (isReactTypeMatch(name, 'Ref', defaultImportName, reactImportedName)) {
return createCall(
t.identifier('oneOfType'),
[
Expand Down
27 changes: 18 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ import extractTypeProperties from './extractTypeProperties';
// import { loadProgram } from './typeChecker';
import upsertImport from './upsertImport';
import { Path, PluginOptions, ConvertState, PropTypeDeclaration } from './types';
import {
checkForUnsupportedPragma,
getClassComponentNamesFromPragma,
getDefaultImportNameFromPragma,
getFcNamesFromPragma,
} from './jsx-pragma';

const BABEL_VERSION = 7;
const MAX_DEPTH = 3;
const MAX_SIZE = 25;
const REACT_FC_NAMES = ['SFC', 'StatelessComponent', 'FC', 'FunctionComponent'];

function isNotTS(name: string): boolean {
return name.endsWith('.js') || name.endsWith('.jsx');
Expand Down Expand Up @@ -75,6 +80,7 @@ export default declare((api: any, options: PluginOptions, root: string) => {
comments: false,
customPropTypeSuffixes: [],
forbidExtraProps: false,
jsxPragma: 'react',
maxDepth: MAX_DEPTH,
maxSize: MAX_SIZE,
strict: true,
Expand All @@ -89,6 +95,7 @@ export default declare((api: any, options: PluginOptions, root: string) => {
reactImportedName: '',
referenceTypes: {},
};
checkForUnsupportedPragma(((this as any).state as ConvertState).options.jsxPragma);
},

visitor: {
Expand Down Expand Up @@ -132,9 +139,9 @@ export default declare((api: any, options: PluginOptions, root: string) => {
state.airbnbPropTypes.forbidImport = response.namedImport;
}

if (node.source.value === 'react') {
if (node.source.value === state.options.jsxPragma) {
const response = upsertImport(node, {
checkForDefault: 'React',
checkForDefault: getDefaultImportNameFromPragma(state.options.jsxPragma),
});

state.reactImportedName = response.defaultImport;
Expand Down Expand Up @@ -190,21 +197,22 @@ export default declare((api: any, options: PluginOptions, root: string) => {
// @ts-expect-error
'ClassDeclaration|ClassExpression': (path: Path<t.ClassDeclaration>) => {
const { node } = path;

const classComponentNames = getClassComponentNamesFromPragma(state.options.jsxPragma);

// prettier-ignore
const valid = node.superTypeParameters && (
// React.Component, React.PureComponent
(
t.isMemberExpression(node.superClass) &&
t.isIdentifier(node.superClass.object, { name: state.reactImportedName }) && (
t.isIdentifier(node.superClass.property, { name: 'Component' }) ||
t.isIdentifier(node.superClass.property, { name: 'PureComponent' })
classComponentNames.some(name => t.isIdentifier((node.superClass as t.MemberExpression).property, { name }))
)
) ||
// Component, PureComponent
(
state.reactImportedName && (
t.isIdentifier(node.superClass, { name: 'Component' }) ||
t.isIdentifier(node.superClass, { name: 'PureComponent' })
classComponentNames.some(name => t.isIdentifier(node.superClass, { name }))
)
)
);
Expand Down Expand Up @@ -307,6 +315,7 @@ export default declare((api: any, options: PluginOptions, root: string) => {
// const Foo: React.FC<Props> = () => {};
if (id?.typeAnnotation?.typeAnnotation) {
const type = id.typeAnnotation.typeAnnotation;
const fcNames = getFcNamesFromPragma(state.options.jsxPragma);

if (
t.isTSTypeReference(type) &&
Expand All @@ -323,14 +332,14 @@ export default declare((api: any, options: PluginOptions, root: string) => {
t.isIdentifier(type.typeName.left, {
name: state.reactImportedName,
}) &&
REACT_FC_NAMES.some((name) =>
fcNames.some((name) =>
// @ts-expect-error TODO: revisit once babel types stabilize
t.isIdentifier((type.typeName as any).right, { name }),
)) ||
// FC, FunctionComponent
(!!state.reactImportedName &&
// @ts-expect-error TODO: revisit once babel types stabilize
REACT_FC_NAMES.some((name) => t.isIdentifier(type.typeName, { name }))))
fcNames.some((name) => t.isIdentifier(type.typeName, { name }))))
) {
// @ts-expect-error TODO: revisit once babel types stabilize
props = type.typeParameters.params[0];
Expand Down
71 changes: 71 additions & 0 deletions src/jsx-pragma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { JsxPragma } from './types';

const JSXPragmaToFcNamesMap: Record<JsxPragma, string[]> = {
preact: ['ComponentFactory', 'FunctionComponent', 'FunctionalComponent'],
react: ['SFC', 'StatelessComponent', 'FC', 'FunctionComponent'],
};

const JSXPragmaToClassComponentNamesMap: Record<JsxPragma, string[]> = {
preact: ['Component', 'AnyComponent', 'ComponentClass', 'ComponentConstructor'],
react: ['Component', 'PureComponent'],
};

const JSXPragmaToFcDefaultImportNameMap: Record<JsxPragma, string> = {
preact: 'preact',
react: 'React',
};

const JSXPragmaToNodeTypesMap: Record<JsxPragma, string[]> = {
preact: ['VNode', 'Text', 'Element', 'JSX.Element', 'JSX.ElementClass'],
react: ['ReactText', 'ReactNode', 'ReactType', 'ElementType'],
};

const JSXPragmaToFunctionTypesMap: Record<JsxPragma, string[]> = {
preact: ['VNode', 'ComponentType', 'ComponentClass', 'ComponentConstructor', 'AnyComponent', 'ComponentType', 'Element', 'JSX.Element', 'JSX.ElementClass'],
react: ['ComponentType', 'ComponentClass', 'StatelessComponent', 'ElementType'],
};

const JSXPragmaToElementTypesMap: Record<JsxPragma, string[]> = {
preact: ['VNode', 'Element', 'JSX.Element', 'JSX.ElementClass'],
react: [
'ReactElement',
'ComponentElement',
'FunctionComponentElement',
'DOMElement',
'SFCElement',
],
};

export function checkForUnsupportedPragma(pragma: string) {
if (!JSXPragmaToFcNamesMap[pragma as JsxPragma]) {
throw new Error(
`Unsupported JSX Pragma: ${pragma}, please use one of the following list: [${Object.keys(
JSXPragmaToFcNamesMap,
).join(', ')}]`,
);
}
}

export function getFcNamesFromPragma(pragma: JsxPragma): string[] {
return JSXPragmaToFcNamesMap[pragma];
}

export function getClassComponentNamesFromPragma(pragma: JsxPragma): string[] {
return JSXPragmaToClassComponentNamesMap[pragma];
}

export function getDefaultImportNameFromPragma(pragma: JsxPragma): string {
return JSXPragmaToFcDefaultImportNameMap[pragma];
}

export function getNodeTypesFromPragma(pragma: JsxPragma): string[] {
return JSXPragmaToNodeTypesMap[pragma];
}

export function getFunctionTypesFromPragma(pragma: JsxPragma): string[] {
return JSXPragmaToFunctionTypesMap[pragma];
}

export function getElementTypesFromPragma(pragma: JsxPragma): string[] {
return JSXPragmaToElementTypesMap[pragma];
}
13 changes: 11 additions & 2 deletions src/propTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@ export function hasCustomPropTypeSuffix(name: string, suffixes?: string[]): bool
return !!suffixes && suffixes.some((suffix) => name.endsWith(suffix));
}

export function isReactTypeMatch(name: string, type: string, reactImportedName: string): boolean {
return name === type || name === `React.${type}` || name === `${reactImportedName}.${type}`;
export function isReactTypeMatch(
name: string,
type: string,
defaultImportName: string,
reactImportedName: string,
): boolean {
return (
name === type ||
name === `${defaultImportName}.${type}` ||
name === `${reactImportedName}.${type}`
);
}

export function wrapIsRequired(propType: PropType, optional?: boolean | null): PropType {
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export type PropTypeDeclaration = t.TSTypeReference | t.TSIntersectionType | t.T

export type PropType = t.MemberExpression | t.CallExpression | t.Identifier | t.Literal;

export type JsxPragma = 'react' | 'preact';

export interface PluginOptions {
comments?: boolean;
customPropTypeSuffixes?: string[];
Expand All @@ -20,6 +22,7 @@ export interface PluginOptions {
maxSize?: number;
strict?: boolean;
typeCheck?: boolean | string;
jsxPragma?: JsxPragma;
}

export interface ConvertState {
Expand Down
14 changes: 14 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,20 @@ describe('babel-plugin-typescript-to-proptypes', () => {
),
).toMatchSnapshot();
});

it('emits unsupported pragma error if jsxPragma is not in the supported list', () => {
expect(
() => transform(
path.join(__dirname, './fixtures/special/ts-preset.ts'),
{
presets: ['@babel/preset-typescript'],
},
{
jsxPragma: 'something-else' as any,
},
),
).toThrow(/Unsupported JSX Pragma/u);
});
});

// describe('typeCheck', () => {
Expand Down