import { ASTPath, CallExpression, Collection, Expression, Identifier, JSCodeshift, JSXAttribute, JSXElement, Literal, MemberExpression, ObjectExpression, Property, SpreadElement, } from 'jscodeshift'; import { Node, Operation, RegisterAPI, WarningMessage } from '../types/transformer'; import { FLEX_PLUGIN_CLASS, INIT_METHOD } from '../constants'; import { DOM_MANIPULATORS, DOM_ROOT, DYNAMIC_COMPONENTS } from '../constants/rules'; import { getWarningText } from './helper'; /** * Checks for usage of DOM Manipulation methods * @param root Root Collection of the Source Code AST * @param j JSCodeshift core * @returns List of warning messages */ export const getDOMWarnings = (root: Collection, j: JSCodeshift): string[] => { const warnings: string[] = []; DOM_MANIPULATORS.forEach((method) => { const callExp = root .find(j.Identifier, { name: method, }) .closest(j.CallExpression); callExp.forEach((exp) => { const expCallee = exp.node.callee as MemberExpression; const expStr = j(exp.get()).toSource(); let rootIdentifier: string = ''; // Get the root identifier of the call expression if (expCallee.object.type === 'Identifier') { rootIdentifier = (expCallee.object as Identifier)?.name; } else if (expCallee.object.type === 'MemberExpression') { rootIdentifier = ((expCallee.object as MemberExpression).object as Identifier)?.name; } if (rootIdentifier && expStr) { if (DOM_ROOT.includes(rootIdentifier)) { // Root identifier is a DOM root node warnings.push(getWarningText(WarningMessage.Dom, expStr)); } else { /* * Check if Root identifier is a variable initialised with a DOM root node expression * Example: * const ele = document.querySelector('#foo'); * ele.remove(); */ const variableExp = root .find(j.Identifier, { name: rootIdentifier }) .closest(j.VariableDeclarator) .nodes()[0]; rootIdentifier = (((variableExp.init as CallExpression)?.callee as MemberExpression)?.object as Identifier) ?.name; if (DOM_ROOT.includes(rootIdentifier)) { warnings.push(getWarningText(WarningMessage.Dom, expStr)); } } } }); }); return warnings; }; /** * Returns the CallExpression nodes for add/remove/replace/addWrapper APIs in Flex Dynamic Components * @param flexVariable Flex instance variable * @param root Root Collection of the Source Code AST * @param j JSCodeshift core * @returns Collection of CallExpression nodes */ export const getCallExpressions = ( flexVariable: string, root: Collection, j: JSCodeshift, ): Collection => root .find(j.MemberExpression, { object: { name: flexVariable, }, }) .closest(j.CallExpression); /** * Returns the CallExpression nodes for add/remove/replace/addWrapper APIs in minified plugin code * @param root Root Collection of the Source Code AST * @param j JSCodeshift core * @returns Collection of CallExpression nodes */ export const getMinifiedCallExpressions = (root: Collection, j: JSCodeshift): ASTPath[] => DYNAMIC_COMPONENTS.map((comp) => root .find(j.Identifier, { name: comp.split('.')[0], }) .closest(j.CallExpression), ) .flat() .filter((exp) => exp.paths().length > 0) .map((exp) => exp.paths().find((p) => p.node.type === 'CallExpression')) as ASTPath[]; /** * Returns the Dynamic Component name * @param node MemberExpression node which is present in CallExpression callee * @param retrieveFlexVariable Should return flex instance variable * @returns Dynamic Component name */ export const getComponent = (node: MemberExpression, retrieveFlexVariable: boolean = false): string[] | string => { let flexVariable = ''; const getComponentString = (addNode: Node): string => { const nodeObject = addNode.object; if (!nodeObject || !nodeObject.property?.name) { flexVariable = nodeObject?.name || ''; return ''; } return `${getComponentString(nodeObject)}.${nodeObject.property.name}`; }; return retrieveFlexVariable ? [getComponentString(node as unknown as Node).slice(1), flexVariable] : getComponentString(node as unknown as Node).slice(1); }; /** * Returns the key prop value of the custom component used in the Dynamic Component API * @param jsxNode JSXElement Node present in the arguments of the CallExpression node * @returns Key of the custom component */ export const geComponentKey = (jsxNode: JSXElement): string => { if (!jsxNode || !jsxNode.openingElement?.attributes?.length) { return ''; } const keyAttributeNode = jsxNode.openingElement.attributes.find( (attr) => attr.type === 'JSXAttribute' && attr.name?.name === 'key', ) as JSXAttribute; const valueNode = keyAttributeNode?.value as Literal; if (typeof valueNode?.value === 'string') { return valueNode.value; } return ''; }; /** * Returns the key prop value of the custom component used in the Dynamic Component API in a minified plugin * @param args Arguments of the CallExpression node * @returns Key prop value of the custom component */ export const getMinifiedComponentKey = (args: (SpreadElement | Expression)[]): string => { const props = ( (args.find((arg) => arg.type === 'CallExpression') as CallExpression)?.arguments?.find( (arg) => arg.type === 'ObjectExpression', ) as ObjectExpression )?.properties as Property[]; return ((props?.find((p) => (p.key as Identifier)?.name === 'key')?.value as Literal)?.value as string) || ''; }; export const getFlexVariable = (root: Collection, j: JSCodeshift): string => { const pluginInitMethod = root .find(j.Identifier, { name: FLEX_PLUGIN_CLASS, }) .closest(j.ClassDeclaration) .find(j.Identifier, { name: INIT_METHOD, }) .closest(j.ClassMethod); const initNode = pluginInitMethod.nodes()[0]; if (initNode) { const flexVariable = (initNode.params[0] as Identifier)?.name; if (!flexVariable) { throw new Error('Could not find Flex instance variable in the `init` method'); } return flexVariable; } return ''; }; /** * Builds uiConfig from ObjectExpresion node * @param objectExpression ObjectExpression from a migrated plugin AST * @param registerMethod Register method used in the migrated plugin * @returns config properties in the ObjectExpression and operation performed for the given CallExpression */ export const getUIConfig = ( objectExpression: ObjectExpression, registerMethod: RegisterAPI, ): { config: Record; operation: Operation } => { const config: Record = {}; objectExpression.properties.forEach((prop) => { if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && prop.value.type === 'StringLiteral') { config[prop.key.name] = prop.value.value; } }); let operation: Operation = config.operationHint as Operation; if (!operation) { operation = registerMethod === RegisterAPI.Remove ? Operation.Remove : Operation.Wrap; } return { config, operation }; };