| | import type { |
| | API, |
| | Collection, |
| | ASTPath, |
| | ObjectPattern, |
| | Identifier, |
| | } from 'jscodeshift' |
| | import { |
| | determineClientDirective, |
| | generateUniqueIdentifier, |
| | getFunctionPathFromExportPath, |
| | insertReactUseImport, |
| | isFunctionType, |
| | TARGET_NAMED_EXPORTS, |
| | TARGET_PROP_NAMES, |
| | turnFunctionReturnTypeToAsync, |
| | wrapParentheseIfNeeded, |
| | type FunctionScope, |
| | insertCommentOnce, |
| | TARGET_ROUTE_EXPORTS, |
| | getVariableDeclaratorId, |
| | NEXTJS_ENTRY_FILES, |
| | NEXT_CODEMOD_ERROR_PREFIX, |
| | findFunctionBody, |
| | containsReactHooksCallExpressions, |
| | isParentUseCallExpression, |
| | isParentPromiseAllCallExpression, |
| | findClosetParentFunctionScope, |
| | } from './utils' |
| | import { createParserFromPath } from '../../../lib/parser' |
| |
|
| | const PAGE_PROPS = 'props' |
| |
|
| | |
| | |
| | |
| | function awaitMemberAccessOfProp( |
| | propIdName: string, |
| | path: ASTPath<FunctionScope>, |
| | j: API['jscodeshift'] |
| | ) { |
| | |
| | const functionBody = findFunctionBody(path) |
| | const memberAccess = j(functionBody).find(j.MemberExpression, { |
| | object: { |
| | type: 'Identifier', |
| | name: propIdName, |
| | }, |
| | }) |
| |
|
| | let hasAwaited = false |
| | |
| | memberAccess.forEach((memberAccessPath) => { |
| | const member = memberAccessPath.value |
| |
|
| | const memberProperty = member.property |
| | const isAccessingMatchedProperty = |
| | j.Identifier.check(memberProperty) && |
| | TARGET_PROP_NAMES.has(memberProperty.name) |
| |
|
| | if (!isAccessingMatchedProperty) { |
| | return |
| | } |
| |
|
| | if (isParentPromiseAllCallExpression(memberAccessPath, j)) { |
| | return |
| | } |
| |
|
| | |
| | if (memberAccessPath.parentPath?.value.type === 'AwaitExpression') { |
| | return |
| | } |
| |
|
| | const parentScopeOfMemberAccess = findClosetParentFunctionScope( |
| | memberAccessPath, |
| | j |
| | ) |
| |
|
| | |
| | if ( |
| | parentScopeOfMemberAccess && |
| | !parentScopeOfMemberAccess.value?.async && |
| | parentScopeOfMemberAccess.node !== path.node |
| | ) { |
| | |
| | |
| | const comment = ` ${NEXT_CODEMOD_ERROR_PREFIX} '${propIdName}.${memberProperty.name}' is accessed without awaiting.` |
| | insertCommentOnce(member, j, comment) |
| | return |
| | } |
| |
|
| | const awaitedExpr = j.awaitExpression(member) |
| |
|
| | const awaitMemberAccess = wrapParentheseIfNeeded(true, j, awaitedExpr) |
| | memberAccessPath.replace(awaitMemberAccess) |
| | hasAwaited = true |
| | }) |
| |
|
| | const hasReactHooksUsage = containsReactHooksCallExpressions( |
| | path.get('body'), |
| | j |
| | ) |
| |
|
| | |
| | if (hasAwaited) { |
| | if (path.value.async === false && !hasReactHooksUsage) { |
| | path.value.async = true |
| | turnFunctionReturnTypeToAsync(path.value, j) |
| | } |
| | } |
| | return hasAwaited |
| | } |
| |
|
| | function applyUseAndRenameAccessedProp( |
| | propIdName: string, |
| | path: ASTPath<FunctionScope>, |
| | j: API['jscodeshift'] |
| | ) { |
| | |
| | |
| | |
| | |
| | |
| | let modified = false |
| | const functionBody = findFunctionBody(path) |
| | const memberAccess = j(functionBody).find(j.MemberExpression, { |
| | object: { |
| | type: 'Identifier', |
| | name: propIdName, |
| | }, |
| | }) |
| |
|
| | const accessedNames: string[] = [] |
| | |
| | memberAccess.forEach((memberAccessPath) => { |
| | |
| | if (isParentUseCallExpression(memberAccessPath, j)) { |
| | return |
| | } |
| |
|
| | const member = memberAccessPath.value |
| | const memberProperty = member.property |
| | if (j.Identifier.check(memberProperty)) { |
| | accessedNames.push(memberProperty.name) |
| | } else if (j.MemberExpression.check(memberProperty)) { |
| | let currentMember = memberProperty |
| | if (j.Identifier.check(currentMember.object)) { |
| | accessedNames.push(currentMember.object.name) |
| | } |
| | } |
| | memberAccessPath.replace(memberProperty) |
| | }) |
| |
|
| | |
| | |
| | if (accessedNames.length > 0) { |
| | const accessedPropId = j.identifier(propIdName) |
| | const accessedProp = j.memberExpression( |
| | accessedPropId, |
| | j.identifier(accessedNames[0]) |
| | ) |
| |
|
| | const useCall = j.callExpression(j.identifier('use'), [accessedProp]) |
| | const useDeclaration = j.variableDeclaration('const', [ |
| | j.variableDeclarator(j.identifier(accessedNames[0]), useCall), |
| | ]) |
| |
|
| | if (functionBody) { |
| | functionBody.unshift(useDeclaration) |
| | } |
| |
|
| | modified = true |
| | } |
| | return modified |
| | } |
| |
|
| | function commentOnMatchedReExports( |
| | root: Collection<any>, |
| | j: API['jscodeshift'] |
| | ): boolean { |
| | let modified = false |
| | root.find(j.ExportNamedDeclaration).forEach((path) => { |
| | if (j.ExportSpecifier.check(path.value.specifiers[0])) { |
| | const specifiers = path.value.specifiers |
| | for (const specifier of specifiers) { |
| | if ( |
| | j.ExportSpecifier.check(specifier) && |
| | |
| | (TARGET_NAMED_EXPORTS.has(specifier.exported.name) || |
| | specifier.exported.name === 'default') |
| | ) { |
| | if (j.Literal.check(path.value.source)) { |
| | const localName = specifier.local.name |
| |
|
| | const commentInserted = insertCommentOnce( |
| | specifier, |
| | j, |
| | ` ${NEXT_CODEMOD_ERROR_PREFIX} \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\`` |
| | ) |
| | modified ||= commentInserted |
| | } else if (path.value.source === null) { |
| | const localIdentifier = specifier.local |
| | const localName = localIdentifier.name |
| | |
| | const importDeclaration = root |
| | .find(j.ImportDeclaration) |
| | .filter((importPath) => { |
| | return importPath.value.specifiers.some( |
| | (importSpecifier) => importSpecifier.local.name === localName |
| | ) |
| | }) |
| | if (importDeclaration.size() > 0) { |
| | const commentInserted = insertCommentOnce( |
| | specifier, |
| | j, |
| | ` ${NEXT_CODEMOD_ERROR_PREFIX} \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\`` |
| | ) |
| | modified ||= commentInserted |
| | } |
| | } |
| | } |
| | } |
| | } |
| | }) |
| | return modified |
| | } |
| |
|
| | function modifyTypes( |
| | paramTypeAnnotation: any, |
| | propsIdentifier: Identifier, |
| | root: Collection<any>, |
| | j: API['jscodeshift'] |
| | ): boolean { |
| | let modified = false |
| | if (paramTypeAnnotation && paramTypeAnnotation.typeAnnotation) { |
| | const typeAnnotation = paramTypeAnnotation.typeAnnotation |
| | if (typeAnnotation.type === 'TSTypeLiteral') { |
| | const typeLiteral = typeAnnotation |
| |
|
| | |
| | typeLiteral.members.forEach((member) => { |
| | if ( |
| | member.type === 'TSPropertySignature' && |
| | member.key.type === 'Identifier' && |
| | TARGET_PROP_NAMES.has(member.key.name) |
| | ) { |
| | |
| | if ( |
| | member.typeAnnotation && |
| | member.typeAnnotation.typeAnnotation && |
| | member.typeAnnotation.typeAnnotation.type === 'TSTypeReference' && |
| | member.typeAnnotation.typeAnnotation.typeName.type === |
| | 'Identifier' && |
| | member.typeAnnotation.typeAnnotation.typeName.name === 'Promise' |
| | ) { |
| | return |
| | } |
| |
|
| | |
| | if ( |
| | member.typeAnnotation && |
| | member.typeAnnotation.typeAnnotation && |
| | j.TSType.check(member.typeAnnotation.typeAnnotation) |
| | ) { |
| | member.typeAnnotation.typeAnnotation = j.tsTypeReference( |
| | j.identifier('Promise'), |
| | j.tsTypeParameterInstantiation([ |
| | member.typeAnnotation.typeAnnotation, |
| | ]) |
| | ) |
| | modified = true |
| | } |
| | } |
| | }) |
| | } else if (typeAnnotation.type === 'TSTypeReference') { |
| | |
| | |
| | const typeReference = typeAnnotation |
| | if (typeReference.typeName.type === 'Identifier') { |
| | |
| | const foundTypes = findAllTypes(root, j, typeReference.typeName.name) |
| |
|
| | |
| | if (foundTypes.interfaces.length > 0) { |
| | const interfaceDeclaration = foundTypes.interfaces[0] |
| | if ( |
| | interfaceDeclaration.type === 'TSInterfaceDeclaration' && |
| | interfaceDeclaration.body?.type === 'TSInterfaceBody' |
| | ) { |
| | const typeBody = interfaceDeclaration.body.body |
| | |
| | |
| | typeBody.forEach((member) => { |
| | if ( |
| | member.type === 'TSPropertySignature' && |
| | member.key.type === 'Identifier' && |
| | TARGET_PROP_NAMES.has(member.key.name) |
| | ) { |
| | |
| | if ( |
| | member.typeAnnotation && |
| | member.typeAnnotation.typeAnnotation && |
| | member.typeAnnotation?.typeAnnotation?.typeName?.name === |
| | 'Promise' |
| | ) { |
| | return |
| | } |
| |
|
| | |
| | if ( |
| | member.typeAnnotation && |
| | member.typeAnnotation.typeAnnotation && |
| | |
| | TARGET_PROP_NAMES.has(member.key.name) |
| | ) { |
| | member.typeAnnotation.typeAnnotation = j.tsTypeReference( |
| | j.identifier('Promise'), |
| | j.tsTypeParameterInstantiation([ |
| | member.typeAnnotation.typeAnnotation, |
| | ]) |
| | ) |
| | modified = true |
| | } |
| | } |
| | }) |
| | } |
| | } |
| |
|
| | |
| | if (foundTypes.typeAliases.length > 0) { |
| | const typeAliasDeclaration = foundTypes.typeAliases[0] |
| | if (j.TSTypeAliasDeclaration.check(typeAliasDeclaration)) { |
| | const typeAlias = typeAliasDeclaration.typeAnnotation |
| | if ( |
| | j.TSTypeLiteral.check(typeAlias) && |
| | typeAlias.members.length > 0 |
| | ) { |
| | const typeLiteral = typeAlias |
| | typeLiteral.members.forEach((member) => { |
| | if ( |
| | j.TSPropertySignature.check(member) && |
| | j.Identifier.check(member.key) && |
| | TARGET_PROP_NAMES.has(member.key.name) |
| | ) { |
| | |
| | if ( |
| | member.typeAnnotation && |
| | member.typeAnnotation.typeAnnotation && |
| | member.typeAnnotation.typeAnnotation.type === |
| | 'TSTypeReference' && |
| | member.typeAnnotation.typeAnnotation.typeName.type === |
| | 'Identifier' && |
| | member.typeAnnotation.typeAnnotation.typeName.name === |
| | 'Promise' |
| | ) { |
| | return |
| | } |
| |
|
| | |
| | if ( |
| | member.typeAnnotation && |
| | j.TSTypeLiteral.check(member.typeAnnotation.typeAnnotation) |
| | ) { |
| | member.typeAnnotation.typeAnnotation = j.tsTypeReference( |
| | j.identifier('Promise'), |
| | j.tsTypeParameterInstantiation([ |
| | member.typeAnnotation.typeAnnotation, |
| | ]) |
| | ) |
| | modified = true |
| | } |
| | } |
| | }) |
| | } |
| | } |
| | } |
| |
|
| | if (foundTypes.imports.length > 0) { |
| | |
| | |
| | if (typeReference.typeName.name === 'PropsWithChildren') { |
| | const propType = typeReference.typeParameters?.params[0] |
| | if ( |
| | propType && |
| | j.TSTypeLiteral.check(propType) && |
| | propType.members.length > 0 |
| | ) { |
| | const typeLiteral = propType |
| | typeLiteral.members.forEach((member) => { |
| | if ( |
| | j.TSPropertySignature.check(member) && |
| | j.Identifier.check(member.key) && |
| | TARGET_PROP_NAMES.has(member.key.name) |
| | ) { |
| | |
| | if ( |
| | member.typeAnnotation && |
| | member.typeAnnotation.typeAnnotation && |
| | member.typeAnnotation.typeAnnotation.type === |
| | 'TSTypeReference' && |
| | member.typeAnnotation.typeAnnotation.typeName.type === |
| | 'Identifier' && |
| | member.typeAnnotation.typeAnnotation.typeName.name === |
| | 'Promise' |
| | ) { |
| | return |
| | } |
| |
|
| | |
| | if ( |
| | member.typeAnnotation && |
| | j.TSTypeLiteral.check(member.typeAnnotation.typeAnnotation) |
| | ) { |
| | member.typeAnnotation.typeAnnotation = j.tsTypeReference( |
| | j.identifier('Promise'), |
| | j.tsTypeParameterInstantiation([ |
| | member.typeAnnotation.typeAnnotation, |
| | ]) |
| | ) |
| | modified = true |
| | } |
| | } |
| | }) |
| | } |
| | } |
| | } |
| | } |
| | } |
| |
|
| | propsIdentifier.typeAnnotation = paramTypeAnnotation |
| | modified = true |
| | } |
| | return modified |
| | } |
| |
|
| | export function transformDynamicProps( |
| | source: string, |
| | _api: API, |
| | filePath: string |
| | ) { |
| | const isEntryFile = NEXTJS_ENTRY_FILES.test(filePath) |
| | if (!isEntryFile) { |
| | return null |
| | } |
| |
|
| | let modified = false |
| | let modifiedPropArgument = false |
| | const j = createParserFromPath(filePath) |
| | const root = j(source) |
| | |
| | let needsReactUseImport = false |
| | |
| | |
| | |
| | let insertedDestructPropNames = new Set<string>() |
| |
|
| | function processAsyncPropOfEntryFile(isClientComponent: boolean) { |
| | |
| | function renameAsyncPropIfExisted( |
| | path: ASTPath<FunctionScope>, |
| | isDefaultExport: boolean |
| | ) { |
| | const decl = path.value |
| | const params = decl.params |
| | let functionName = decl.id?.name |
| | |
| | if (!decl.id) { |
| | functionName = getVariableDeclaratorId(path, j)?.name |
| | } |
| | |
| | const propertiesMap = new Map<string, any>() |
| | let allProperties: ObjectPattern['properties'] = [] |
| | const isRoute = !isDefaultExport && TARGET_ROUTE_EXPORTS.has(functionName) |
| | |
| | if (functionName === 'generateMetadata') { |
| | if (params.length > 2 || params.length === 0) return |
| | } else if (isRoute) { |
| | if (params.length !== 2) return |
| | } else { |
| | |
| | if (params.length !== 1) return |
| | } |
| | const propsIdentifier = generateUniqueIdentifier(PAGE_PROPS, path, j) |
| |
|
| | const propsArgumentIndex = isRoute ? 1 : 0 |
| |
|
| | const currentParam = params[propsArgumentIndex] |
| | if (!currentParam) return |
| |
|
| | |
| | if (currentParam.type === 'ObjectPattern') { |
| | |
| | |
| | let foundTargetProp = false |
| | for (const prop of currentParam.properties) { |
| | if ('key' in prop && prop.key.type === 'Identifier') { |
| | const propName = prop.key.name |
| | if (TARGET_PROP_NAMES.has(propName)) { |
| | foundTargetProp = true |
| | } |
| | } |
| | } |
| |
|
| | |
| | if (!foundTargetProp) return |
| |
|
| | allProperties = currentParam.properties |
| | currentParam.properties.forEach((prop) => { |
| | if ( |
| | |
| | (j.Property.check(prop) || j.ObjectProperty.check(prop)) && |
| | j.Identifier.check(prop.key) && |
| | TARGET_PROP_NAMES.has(prop.key.name) |
| | ) { |
| | const value = 'value' in prop ? prop.value : null |
| | propertiesMap.set(prop.key.name, value) |
| | } |
| | }) |
| |
|
| | modifiedPropArgument = true |
| | } else if (currentParam.type === 'Identifier') { |
| | |
| | |
| | |
| | const argName = currentParam.name |
| |
|
| | if (isClientComponent) { |
| | const modifiedProp = applyUseAndRenameAccessedProp(argName, path, j) |
| | if (modifiedProp) { |
| | needsReactUseImport = true |
| | modified = true |
| | } |
| | } else { |
| | |
| | |
| | |
| | const awaited = awaitMemberAccessOfProp(argName, path, j) |
| | modified ||= awaited |
| | } |
| |
|
| | modified ||= modifyTypes( |
| | currentParam.typeAnnotation, |
| | propsIdentifier, |
| | root, |
| | j |
| | ) |
| |
|
| | |
| | |
| |
|
| | |
| | const callExpressions = j(path).find(j.CallExpression, { |
| | arguments: (args) => { |
| | return args.some((arg) => { |
| | return ( |
| | j.Identifier.check(arg) && |
| | arg.name === argName && |
| | arg.type === 'Identifier' |
| | ) |
| | }) |
| | }, |
| | }) |
| |
|
| | |
| | callExpressions.forEach((callExpression) => { |
| | |
| | const args = callExpression.value.arguments |
| | const propPassedAsArg = args.find( |
| | (arg) => j.Identifier.check(arg) && arg.name === argName |
| | ) |
| | const comment = ` ${NEXT_CODEMOD_ERROR_PREFIX} '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. ` |
| | const inserted = insertCommentOnce(propPassedAsArg, j, comment) |
| | modified ||= inserted |
| | }) |
| |
|
| | if (modified) { |
| | modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j) |
| | } |
| | } |
| |
|
| | if (modifiedPropArgument) { |
| | const isModified = resolveAsyncProp( |
| | path, |
| | propertiesMap, |
| | propsIdentifier.name, |
| | allProperties, |
| | isDefaultExport |
| | ) |
| | if (isModified) { |
| | |
| | if (j.ObjectPattern.check(currentParam)) { |
| | modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j) |
| | } |
| | |
| | params[propsArgumentIndex] = propsIdentifier |
| |
|
| | modified = true |
| | } |
| | } else { |
| | |
| | if (j.Identifier.check(currentParam)) { |
| | const commented = commentSpreadProps(path, currentParam.name, j) |
| | const modifiedTypes = modifyTypes( |
| | currentParam.typeAnnotation, |
| | propsIdentifier, |
| | root, |
| | j |
| | ) |
| | modified ||= commented || modifiedTypes |
| | } |
| | } |
| | } |
| |
|
| | |
| | function resolveAsyncProp( |
| | path: ASTPath<FunctionScope>, |
| | propertiesMap: Map<string, Identifier | ObjectPattern | undefined>, |
| | propsIdentifierName: string, |
| | allProperties: ObjectPattern['properties'], |
| | isDefaultExport: boolean |
| | ): boolean { |
| | |
| | const insertedRenamedPropFunctionNames = new Set<string>() |
| | const node = path.value |
| |
|
| | |
| | if (isDefaultExport && !isClientComponent) { |
| | const hasReactHooksUsage = containsReactHooksCallExpressions( |
| | path.get('body'), |
| | j |
| | ) |
| | if (node.async === false && !hasReactHooksUsage) { |
| | node.async = true |
| | turnFunctionReturnTypeToAsync(node, j) |
| | } |
| | } |
| |
|
| | |
| | if ( |
| | j.ArrowFunctionExpression.check(path.node) && |
| | !j.BlockStatement.check(path.node.body) |
| | ) { |
| | const objectExpression = path.node.body |
| | let hasUsedProps = false |
| | j(objectExpression) |
| | .find(j.Identifier) |
| | .forEach((identifierPath) => { |
| | const idName = identifierPath.value.name |
| | if (propertiesMap.has(idName)) { |
| | hasUsedProps = true |
| | return |
| | } |
| | }) |
| |
|
| | |
| | if (hasUsedProps) { |
| | path.node.body = j.blockStatement([ |
| | j.returnStatement(objectExpression), |
| | ]) |
| | } |
| | } |
| |
|
| | const isAsyncFunc = !!node.async |
| | const functionName = path.value.id?.name || 'default' |
| | const functionBody = findFunctionBody(path) |
| | const functionBodyPath = path.get('body') |
| | const hasReactHooksUsage = containsReactHooksCallExpressions( |
| | functionBodyPath, |
| | j |
| | ) |
| | const hasOtherProperties = allProperties.length > propertiesMap.size |
| |
|
| | function createDestructuringDeclaration( |
| | properties: ObjectPattern['properties'], |
| | destructPropsIdentifierName: string |
| | ) { |
| | const propsToKeep = [] |
| | let restProperty = null |
| |
|
| | |
| | properties.forEach((property) => { |
| | if (j.ObjectProperty.check(property)) { |
| | |
| | const keyName = j.Identifier.check(property.key) |
| | ? property.key.name |
| | : j.Literal.check(property.key) |
| | ? property.key.value |
| | : null |
| |
|
| | if (typeof keyName === 'string') { |
| | propsToKeep.push(property) |
| | } |
| | } else if (j.RestElement.check(property)) { |
| | restProperty = property |
| | } |
| | }) |
| |
|
| | if (propsToKeep.length === 0 && !restProperty) { |
| | return null |
| | } |
| |
|
| | if (restProperty) { |
| | propsToKeep.push(restProperty) |
| | } |
| |
|
| | return j.variableDeclaration('const', [ |
| | j.variableDeclarator( |
| | j.objectPattern(propsToKeep), |
| | j.identifier(destructPropsIdentifierName) |
| | ), |
| | ]) |
| | } |
| |
|
| | if (hasOtherProperties) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const restProperties = allProperties.filter((prop) => { |
| | const isTargetProps = |
| | 'key' in prop && |
| | prop.key.type === 'Identifier' && |
| | TARGET_PROP_NAMES.has(prop.key.name) |
| | return !isTargetProps |
| | }) |
| | const destructionOtherPropertiesDeclaration = |
| | createDestructuringDeclaration(restProperties, propsIdentifierName) |
| | if (functionBody && destructionOtherPropertiesDeclaration) { |
| | functionBody.unshift(destructionOtherPropertiesDeclaration) |
| | } |
| | } |
| |
|
| | let modifiedPropertyCount = 0 |
| | for (const [matchedPropName, paramsProperty] of propertiesMap) { |
| | if (!TARGET_PROP_NAMES.has(matchedPropName)) { |
| | continue |
| | } |
| |
|
| | |
| | if (isClientComponent) { |
| | let shouldSkip = false |
| | const propPaths = j(path).find(j.Identifier, { |
| | name: matchedPropName, |
| | }) |
| |
|
| | for (const propPath of propPaths.paths()) { |
| | if (isParentUseCallExpression(propPath, j)) { |
| | |
| | shouldSkip = true |
| | break |
| | } |
| | } |
| |
|
| | if (shouldSkip) { |
| | continue |
| | } |
| | } |
| |
|
| | const paramsPropertyName = j.Identifier.check(paramsProperty) |
| | ? paramsProperty.name |
| | : null |
| | const paramPropertyName = paramsPropertyName || matchedPropName |
| |
|
| | |
| | |
| | const hasUsedInBody = |
| | j(functionBodyPath) |
| | .find(j.Identifier, { |
| | name: paramPropertyName, |
| | }) |
| | .size() > 0 |
| |
|
| | if (!hasUsedInBody && j.Identifier.check(paramsProperty)) continue |
| |
|
| | |
| | |
| | const propUsages = j(functionBodyPath).find(j.Identifier, { |
| | name: paramPropertyName, |
| | }) |
| |
|
| | |
| | if (propUsages.size()) { |
| | let hasMissingAwaited = false |
| | propUsages.forEach((propUsage) => { |
| | |
| | const isAwaited = |
| | propUsage.parentPath?.value.type === 'AwaitExpression' |
| | const isAwaitedByUse = isParentUseCallExpression(propUsage, j) |
| | if (!isAwaited && !isAwaitedByUse) { |
| | hasMissingAwaited = true |
| | return |
| | } |
| | }) |
| | |
| | if (!hasMissingAwaited) { |
| | continue |
| | } |
| | } |
| |
|
| | modifiedPropertyCount++ |
| |
|
| | const propNameIdentifier = j.identifier(matchedPropName) |
| | const propsIdentifier = j.identifier(propsIdentifierName) |
| | const accessedPropIdExpr = j.memberExpression( |
| | propsIdentifier, |
| | propNameIdentifier |
| | ) |
| | |
| | |
| | |
| | |
| | const uid = functionName + ':' + paramPropertyName |
| |
|
| | if (paramsProperty?.type === 'ObjectPattern') { |
| | const objectPattern = paramsProperty |
| | const objectPatternProperties = objectPattern.properties |
| |
|
| | |
| | const destructedObjectPattern = j.variableDeclaration('const', [ |
| | j.variableDeclarator( |
| | j.objectPattern( |
| | objectPatternProperties.map((prop) => { |
| | if ( |
| | prop.type === 'Property' && |
| | prop.key.type === 'Identifier' |
| | ) { |
| | return j.objectProperty( |
| | j.identifier(prop.key.name), |
| | j.identifier(prop.key.name) |
| | ) |
| | } |
| | return prop |
| | }) |
| | ), |
| | propNameIdentifier |
| | ), |
| | ]) |
| |
|
| | if (!insertedDestructPropNames.has(uid) && functionBody) { |
| | functionBody.unshift(destructedObjectPattern) |
| | insertedDestructPropNames.add(uid) |
| | } |
| | } |
| |
|
| | if (isAsyncFunc) { |
| | |
| | const paramAssignment = j.variableDeclaration('const', [ |
| | j.variableDeclarator( |
| | j.identifier(paramPropertyName), |
| | j.awaitExpression(accessedPropIdExpr) |
| | ), |
| | ]) |
| | if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) { |
| | functionBody.unshift(paramAssignment) |
| | insertedRenamedPropFunctionNames.add(uid) |
| | } |
| | } else { |
| | if ( |
| | !isClientComponent && |
| | isFunctionType(node.type) && |
| | !hasReactHooksUsage |
| | ) { |
| | |
| | node.async = true |
| | turnFunctionReturnTypeToAsync(node, j) |
| | |
| | const paramAssignment = j.variableDeclaration('const', [ |
| | j.variableDeclarator( |
| | j.identifier(paramPropertyName), |
| | j.awaitExpression(accessedPropIdExpr) |
| | ), |
| | ]) |
| |
|
| | if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) { |
| | functionBody.unshift(paramAssignment) |
| | insertedRenamedPropFunctionNames.add(uid) |
| | } |
| | } else { |
| | const paramAssignment = j.variableDeclaration('const', [ |
| | j.variableDeclarator( |
| | j.identifier(paramPropertyName), |
| | j.callExpression(j.identifier('use'), [accessedPropIdExpr]) |
| | ), |
| | ]) |
| | if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) { |
| | needsReactUseImport = true |
| | functionBody.unshift(paramAssignment) |
| | insertedRenamedPropFunctionNames.add(uid) |
| | } |
| | } |
| | } |
| | } |
| |
|
| | return modifiedPropertyCount > 0 |
| | } |
| |
|
| | const defaultExportsDeclarations = root.find(j.ExportDefaultDeclaration) |
| |
|
| | defaultExportsDeclarations.forEach((path) => { |
| | const functionPath = getFunctionPathFromExportPath( |
| | path, |
| | j, |
| | root, |
| | () => true |
| | ) |
| | if (functionPath) { |
| | renameAsyncPropIfExisted(functionPath, true) |
| | } |
| | }) |
| |
|
| | |
| | |
| | |
| | const namedExportDeclarations = root.find(j.ExportNamedDeclaration) |
| |
|
| | namedExportDeclarations.forEach((path) => { |
| | const functionPath = getFunctionPathFromExportPath( |
| | path, |
| | j, |
| | root, |
| | (idName) => TARGET_NAMED_EXPORTS.has(idName) |
| | ) |
| |
|
| | if (functionPath) { |
| | renameAsyncPropIfExisted(functionPath, false) |
| | } |
| | }) |
| | } |
| |
|
| | const isClientComponent = determineClientDirective(root, j) |
| |
|
| | |
| | processAsyncPropOfEntryFile(isClientComponent) |
| |
|
| | |
| | if (needsReactUseImport) { |
| | insertReactUseImport(root, j) |
| | } |
| |
|
| | const commented = commentOnMatchedReExports(root, j) |
| | modified ||= commented |
| |
|
| | return modified ? root.toSource() : null |
| | } |
| |
|
| | function findAllTypes( |
| | root: Collection<any>, |
| | j: API['jscodeshift'], |
| | typeName: string |
| | ) { |
| | const types = { |
| | interfaces: [], |
| | typeAliases: [], |
| | imports: [], |
| | references: [], |
| | } |
| |
|
| | |
| | root |
| | .find(j.TSInterfaceDeclaration, { |
| | id: { |
| | type: 'Identifier', |
| | name: typeName, |
| | }, |
| | }) |
| | .forEach((path) => { |
| | types.interfaces.push(path.node) |
| | }) |
| |
|
| | |
| | root |
| | .find(j.TSTypeAliasDeclaration, { |
| | id: { |
| | type: 'Identifier', |
| | name: typeName, |
| | }, |
| | }) |
| | .forEach((path) => { |
| | types.typeAliases.push(path.node) |
| | }) |
| |
|
| | |
| | root |
| | .find(j.ImportSpecifier, { |
| | imported: { |
| | type: 'Identifier', |
| | name: typeName, |
| | }, |
| | }) |
| | .forEach((path) => { |
| | types.imports.push(path.node) |
| | }) |
| |
|
| | |
| | root |
| | .find(j.TSTypeReference, { |
| | typeName: { |
| | type: 'Identifier', |
| | name: typeName, |
| | }, |
| | }) |
| | .forEach((path) => { |
| | types.references.push(path.node) |
| | }) |
| |
|
| | return types |
| | } |
| |
|
| | function commentSpreadProps( |
| | path: ASTPath<FunctionScope>, |
| | propsIdentifierName: string, |
| | j: API['jscodeshift'] |
| | ): boolean { |
| | let modified = false |
| | const functionBody = findFunctionBody(path) |
| | const functionBodyCollection = j(functionBody) |
| | |
| | const jsxSpreadProperties = functionBodyCollection.find( |
| | j.JSXSpreadAttribute, |
| | { argument: { name: propsIdentifierName } } |
| | ) |
| | const objSpreadProperties = functionBodyCollection.find(j.SpreadElement, { |
| | argument: { name: propsIdentifierName }, |
| | }) |
| | const comment = ` ${NEXT_CODEMOD_ERROR_PREFIX} '${propsIdentifierName}' is used with spread syntax (...). Any asynchronous properties of '${propsIdentifierName}' must be awaited when accessed. ` |
| |
|
| | |
| | jsxSpreadProperties.forEach((spread) => { |
| | const inserted = insertCommentOnce(spread.value, j, comment) |
| | if (inserted) modified = true |
| | }) |
| |
|
| | objSpreadProperties.forEach((spread) => { |
| | const inserted = insertCommentOnce(spread.value, j, comment) |
| | if (inserted) modified = true |
| | }) |
| |
|
| | return modified |
| | } |
| |
|