babel-plugin-typescript-to-proptypes
Version:
Generate React PropTypes from TypeScript prop interfaces.
351 lines (287 loc) • 15 kB
JavaScript
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
const core = require('@babel/core');
const helperModuleImports = require('@babel/helper-module-imports');
const syntaxTypeScript = require('@babel/plugin-syntax-typescript');
const addToClass = require('./addToClass.js');
const addToFunctionOrVar = require('./addToFunctionOrVar.js');
const extractTypeProperties = require('./extractTypeProperties.js');
const upsertImport = require('./upsertImport.js');
const _interopDefault = e => e && e.__esModule ? e : {
default: e
};
const syntaxTypeScript__default = /*#__PURE__*/_interopDefault(syntaxTypeScript);
/* eslint-disable @typescript-eslint/no-explicit-any */
const BABEL_VERSION = 7;
const MAX_DEPTH = 3;
const MAX_SIZE = 25;
const REACT_FC_NAMES = ['SFC', 'StatelessComponent', 'FC', 'FunctionComponent'];
function isNotTS(name) {
return name.endsWith('.js') || name.endsWith('.jsx');
}
function isComponentName(name) {
return !!name.match(/^[A-Z]/u);
}
function isPropsParam(param) {
return (// (props: Props)
core.types.isIdentifier(param) && !!param.typeAnnotation || // ({ ...props }: Props)
core.types.isObjectPattern(param) && !!param.typeAnnotation
);
}
function isPropsType(param) {
return core.types.isTSTypeReference(param) || core.types.isTSIntersectionType(param) || core.types.isTSUnionType(param);
}
const index = (api, options, root) => {
api.assertVersion(BABEL_VERSION);
return {
inherits: syntaxTypeScript__default.default,
manipulateOptions(opts, parserOptions) {
// Some codebases are only partially TypeScript, so we need to support
// regular JS and JSX files, otherwise the Babel parser blows up.
parserOptions.plugins.push('jsx');
},
post() {
// Free up any memory we're hogging
this.state = null;
},
pre() {
// Setup initial state
this.state = {
airbnbPropTypes: {
count: 0,
forbidImport: '',
hasImport: false,
namedImports: []
},
componentTypes: {},
filePath: '',
options: _objectSpread({
comments: false,
customPropTypeSuffixes: [],
forbidExtraProps: false,
mapUnknownReferenceTypesToAny: false,
maxDepth: MAX_DEPTH,
maxSize: MAX_SIZE,
strict: true,
typeCheck: false
}, options),
propTypes: {
count: 0,
defaultImport: '',
hasImport: false
},
reactImportedName: '',
referenceTypes: {}
};
},
visitor: {
Program: {
enter(programPath, {
filename
}) {
const state = this.state;
state.filePath = filename;
if (isNotTS(filename)) {
return;
} // Find existing `react` and `prop-types` imports
programPath.node.body.forEach(node => {
if (!core.types.isImportDeclaration(node)) {
return;
}
if (node.source.value === 'prop-types') {
const response = upsertImport.upsertImport(node, {
checkForDefault: 'PropTypes'
});
state.propTypes.hasImport = true;
state.propTypes.defaultImport = response.defaultImport;
}
if (node.source.value === 'airbnb-prop-types') {
const response = upsertImport.upsertImport(node, {
checkForNamed: 'forbidExtraProps'
});
state.airbnbPropTypes.hasImport = true;
state.airbnbPropTypes.namedImports = response.namedImports;
state.airbnbPropTypes.forbidImport = response.namedImport;
}
if (node.source.value === 'react') {
const response = upsertImport.upsertImport(node);
state.reactImportedName = response.defaultImport;
}
}); // Add `prop-types` import if it does not exist.
// We need to do this without a visitor as we need to modify
// the AST before anything else has can run.
if (!state.propTypes.hasImport && state.reactImportedName) {
state.propTypes.defaultImport = helperModuleImports.addDefault(programPath, 'prop-types', {
nameHint: 'pt'
}).name;
}
if (!state.airbnbPropTypes.hasImport && state.reactImportedName && options.forbidExtraProps) {
state.airbnbPropTypes.forbidImport = helperModuleImports.addNamed(programPath, 'forbidExtraProps', 'airbnb-prop-types').name;
state.airbnbPropTypes.count += 1;
} // Abort early if we're definitely not in a file that needs conversion
if (!state.propTypes.defaultImport && !state.reactImportedName) {
return;
}
const transformers = [];
programPath.traverse({
// airbnbPropTypes.componentWithName()
CallExpression(path) {
const node = path.node;
const namedImports = state.airbnbPropTypes.namedImports;
if (options.forbidExtraProps && core.types.isIdentifier(node.callee) && namedImports.includes(node.callee.name)) {
state.airbnbPropTypes.count += 1;
}
},
// `class Foo extends React.Component<Props> {}`
// @ts-expect-error Union not typed
'ClassDeclaration|ClassExpression': path => {
const node = path.node; // prettier-ignore
const valid = node.superTypeParameters && ( // React.Component, React.PureComponent
core.types.isMemberExpression(node.superClass) && core.types.isIdentifier(node.superClass.object, {
name: state.reactImportedName
}) && (core.types.isIdentifier(node.superClass.property, {
name: 'Component'
}) || core.types.isIdentifier(node.superClass.property, {
name: 'PureComponent'
})) || // Component, PureComponent
state.reactImportedName && (core.types.isIdentifier(node.superClass, {
name: 'Component'
}) || core.types.isIdentifier(node.superClass, {
name: 'PureComponent'
})));
if (valid) {
transformers.push(() => void addToClass.addToClass(node, state));
}
},
// `function Foo(props: Props) {}`
FunctionDeclaration(path) {
const node = path.node;
if (!!state.reactImportedName && node.id && isComponentName(node.id.name) && isPropsParam(node.params[0]) && core.types.isTSTypeAnnotation(node.params[0].typeAnnotation) && isPropsType(node.params[0].typeAnnotation.typeAnnotation)) {
transformers.push(() => void addToFunctionOrVar.addToFunctionOrVar(path, node.id.name, node.params[0].typeAnnotation.typeAnnotation, state));
}
},
// airbnbPropTypes.nonNegativeInteger
Identifier({
node
}) {
const namedImports = state.airbnbPropTypes.namedImports;
if (options.forbidExtraProps && namedImports.includes(node.name)) {
state.airbnbPropTypes.count += 1;
}
},
// PropTypes.*
MemberExpression({
node
}) {
if (core.types.isIdentifier(node.object, {
name: state.propTypes.defaultImport
})) {
state.propTypes.count += 1;
}
},
// `enum Foo {}`
TSEnumDeclaration({
node
}) {
state.referenceTypes[node.id.name] = node;
node.members.forEach(member => {
state.referenceTypes[`${node.id.name}.${member.id.name}`] = member;
});
},
// `interface FooProps {}`
TSInterfaceDeclaration({
node
}) {
state.componentTypes[node.id.name] = extractTypeProperties.extractTypeProperties(node, state.componentTypes);
state.referenceTypes[node.id.name] = node;
},
// `type FooProps = {}`
TSTypeAliasDeclaration({
node
}) {
state.componentTypes[node.id.name] = extractTypeProperties.extractTypeProperties(node, state.componentTypes);
state.referenceTypes[node.id.name] = node;
},
// `const Foo = (props: Props) => {};`
// `const Foo: React.FC<Props> = () => {};`
// `const Ref = React.forwardRef<Element, Props>();`
// `const Memo = React.memo<Props>();`
VariableDeclaration(path) {
const node = path.node;
if (node.declarations.length === 0) {
return;
}
const decl = node.declarations[0];
const id = decl.id;
let props = null; // const Foo: React.FC<Props> = () => {};
if (id !== null && id !== void 0 && id.typeAnnotation && core.types.isTSTypeAnnotation(id.typeAnnotation) && id !== null && id !== void 0 && id.typeAnnotation.typeAnnotation) {
const type = id.typeAnnotation.typeAnnotation;
if (core.types.isTSTypeReference(type) && !!type.typeParameters && type.typeParameters.params.length > 0 && isPropsType(type.typeParameters.params[0]) && ( // React.FC, React.FunctionComponent
core.types.isTSQualifiedName(type.typeName) && core.types.isIdentifier(type.typeName.left, {
name: state.reactImportedName
}) && REACT_FC_NAMES.some(name => core.types.isIdentifier(type.typeName.right, {
name
})) || // FC, FunctionComponent
!!state.reactImportedName && REACT_FC_NAMES.some(name => core.types.isIdentifier(type.typeName, {
name
})))) {
props = type.typeParameters.params[0];
} // const Foo = (props: Props) => {};
// const Foo = function(props: Props) {};
} else if (core.types.isArrowFunctionExpression(decl.init) || core.types.isFunctionExpression(decl.init)) {
if (!!state.reactImportedName && isComponentName(id.name) && isPropsParam(decl.init.params[0]) && core.types.isTSTypeAnnotation(decl.init.params[0].typeAnnotation) && isPropsType(decl.init.params[0].typeAnnotation.typeAnnotation)) {
props = decl.init.params[0].typeAnnotation.typeAnnotation;
} // const Ref = React.forwardRef();
// const Memo = React.memo<Props>();
} else if (core.types.isCallExpression(decl.init)) {
const init = decl.init;
const typeParameters = init.typeParameters;
if (core.types.isMemberExpression(init.callee) && core.types.isIdentifier(init.callee.object) && core.types.isIdentifier(init.callee.property) && init.callee.object.name === state.reactImportedName) {
if (init.callee.property.name === 'forwardRef') {
// const Ref = React.forwardRef<Element, Props>();
if (!!typeParameters && core.types.isTSTypeParameterInstantiation(typeParameters) && typeParameters.params.length > 1 && isPropsType(typeParameters.params[1])) {
props = typeParameters.params[1]; // const Ref = React.forwardRef((props: Props) => {});
} else if (core.types.isArrowFunctionExpression(init.arguments[0]) && init.arguments[0].params.length > 0 && isPropsParam(init.arguments[0].params[0]) && core.types.isTSTypeAnnotation(init.arguments[0].params[0].typeAnnotation) && isPropsType(init.arguments[0].params[0].typeAnnotation.typeAnnotation)) {
props = init.arguments[0].params[0].typeAnnotation.typeAnnotation;
}
} else if (init.callee.property.name === 'memo') {
// const Ref = React.memo<Props>();
if (!!typeParameters && core.types.isTSTypeParameterInstantiation(typeParameters) && typeParameters.params.length > 0 && isPropsType(typeParameters.params[0])) {
props = typeParameters.params[0]; // const Ref = React.memo((props: Props) => {});
} else if (core.types.isArrowFunctionExpression(init.arguments[0]) && init.arguments[0].params.length > 0 && isPropsParam(init.arguments[0].params[0]) && core.types.isTSTypeAnnotation(init.arguments[0].params[0].typeAnnotation) && isPropsType(init.arguments[0].params[0].typeAnnotation.typeAnnotation)) {
props = init.arguments[0].params[0].typeAnnotation.typeAnnotation;
}
}
}
}
if (props) {
transformers.push(() => void addToFunctionOrVar.addToFunctionOrVar(path, id.name, props, state));
}
}
}); // After we have extracted all our information, run all transformers
transformers.forEach(transformer => {
transformer();
});
},
exit(path, {
filename
}) {
const state = this.state;
if (isNotTS(filename)) {
return;
} // Remove the `prop-types` import if no components exist,
// and be sure not to remove pre-existing imports.
path.get('body').forEach(bodyPath => {
if (state.propTypes.count === 0 && core.types.isImportDeclaration(bodyPath.node) && bodyPath.node.specifiers.length <= 1 && bodyPath.node.source.value === 'prop-types') {
bodyPath.remove();
}
});
}
}
}
};
};
module.exports = index;
//# sourceMappingURL=index.js.map
;