UNPKG

babel-plugin-typescript-to-proptypes

Version:
351 lines (287 loc) 15 kB
'use strict'; 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