| | import type { |
| | API, |
| | ArrowFunctionExpression, |
| | ASTPath, |
| | Collection, |
| | ExportDefaultDeclaration, |
| | ExportNamedDeclaration, |
| | FunctionDeclaration, |
| | FunctionExpression, |
| | } from 'jscodeshift' |
| |
|
| | export const NEXTJS_ENTRY_FILES = |
| | /([\\/]|^)(page|layout|route|default)\.(t|j)sx?$/ |
| |
|
| | export type FunctionScope = |
| | | FunctionDeclaration |
| | | FunctionExpression |
| | | ArrowFunctionExpression |
| |
|
| | export const NEXT_CODEMOD_ERROR_PREFIX = '@next-codemod-error' |
| | const NEXT_CODEMOD_IGNORE_ERROR_PREFIX = '@next-codemod-ignore' |
| |
|
| | export const TARGET_ROUTE_EXPORTS = new Set([ |
| | 'GET', |
| | 'POST', |
| | 'PUT', |
| | 'PATCH', |
| | 'DELETE', |
| | 'OPTIONS', |
| | 'HEAD', |
| | ]) |
| |
|
| | export const TARGET_NAMED_EXPORTS = new Set([ |
| | |
| | 'generateMetadata', |
| | 'generateViewport', |
| | ...TARGET_ROUTE_EXPORTS, |
| | ]) |
| |
|
| | export const TARGET_PROP_NAMES = new Set(['params', 'searchParams']) |
| |
|
| | export function isFunctionType( |
| | type: string |
| | ): type is |
| | | 'FunctionDeclaration' |
| | | 'FunctionExpression' |
| | | 'ArrowFunctionExpression' { |
| | return ( |
| | type === 'FunctionDeclaration' || |
| | type === 'FunctionExpression' || |
| | type === 'ArrowFunctionExpression' |
| | ) |
| | } |
| |
|
| | export function isMatchedFunctionExported( |
| | path: ASTPath<FunctionDeclaration>, |
| | j: API['jscodeshift'] |
| | ): boolean { |
| | const matchedFunctionNameFilter = (idName) => TARGET_NAMED_EXPORTS.has(idName) |
| |
|
| | const directNamedExport = j(path).closest(j.ExportNamedDeclaration, { |
| | declaration: { |
| | type: 'FunctionDeclaration', |
| | id: { |
| | name: matchedFunctionNameFilter, |
| | }, |
| | }, |
| | }) |
| |
|
| | if (directNamedExport.size() > 0) { |
| | return true |
| | } |
| |
|
| | |
| | const isDefaultExport = j(path).closest(j.ExportDefaultDeclaration).size() > 0 |
| | if (isDefaultExport) { |
| | return true |
| | } |
| |
|
| | |
| | const root = j(path).closestScope().closest(j.Program) |
| | const isNamedExport = |
| | root |
| | .find(j.ExportNamedDeclaration, { |
| | specifiers: [ |
| | { |
| | type: 'ExportSpecifier', |
| | exported: { |
| | name: matchedFunctionNameFilter, |
| | }, |
| | }, |
| | ], |
| | }) |
| | .size() > 0 |
| |
|
| | |
| | |
| | const isVariableExport = |
| | root |
| | .find(j.ExportNamedDeclaration, { |
| | declaration: { |
| | declarations: [ |
| | { |
| | type: 'VariableDeclarator', |
| | id: { |
| | type: 'Identifier', |
| | name: matchedFunctionNameFilter, |
| | }, |
| | init: { |
| | type: isFunctionType, |
| | }, |
| | }, |
| | ], |
| | }, |
| | }) |
| | .size() > 0 |
| |
|
| | if (isVariableExport) return true |
| |
|
| | return isNamedExport |
| | } |
| |
|
| | |
| | |
| | export function determineClientDirective(root: Collection<any>, j: API['j']) { |
| | const { program } = root.get().node |
| |
|
| | const directive = program.directives[0] |
| | if (j.Directive.check(directive)) { |
| | return directive.value.value === 'use client' |
| | } |
| |
|
| | return false |
| | } |
| |
|
| | export function isPromiseType(typeAnnotation) { |
| | return ( |
| | typeAnnotation.type === 'TSTypeReference' && |
| | typeAnnotation.typeName.name === 'Promise' |
| | ) |
| | } |
| |
|
| | export function turnFunctionReturnTypeToAsync( |
| | node: any, |
| | j: API['jscodeshift'] |
| | ) { |
| | if ( |
| | j.FunctionDeclaration.check(node) || |
| | j.FunctionExpression.check(node) || |
| | j.ArrowFunctionExpression.check(node) |
| | ) { |
| | if (node.returnType) { |
| | const returnTypeAnnotation = node.returnType.typeAnnotation |
| | const isReturnTypePromise = isPromiseType(returnTypeAnnotation) |
| | |
| | |
| | |
| | if (!isReturnTypePromise) { |
| | if ( |
| | node.returnType && |
| | j.TSTypeAnnotation.check(node.returnType) && |
| | (j.TSTypeReference.check(node.returnType.typeAnnotation) || |
| | j.TSUnionType.check(node.returnType.typeAnnotation) || |
| | j.TSTypePredicate.check(node.returnType.typeAnnotation)) |
| | ) { |
| | |
| | node.returnType.typeAnnotation = j.tsTypeReference( |
| | j.identifier('Promise'), |
| | |
| | j.tsTypeParameterInstantiation([returnTypeAnnotation]) |
| | ) |
| | } |
| | } |
| | } |
| | } |
| | } |
| |
|
| | export function insertReactUseImport(root: Collection<any>, j: API['j']) { |
| | const hasReactUseImport = |
| | root |
| | .find(j.ImportSpecifier, { |
| | imported: { |
| | type: 'Identifier', |
| | name: 'use', |
| | }, |
| | }) |
| | .size() > 0 |
| |
|
| | if (!hasReactUseImport) { |
| | const reactImportDeclaration = root.find(j.ImportDeclaration, { |
| | source: { |
| | value: 'react', |
| | }, |
| | |
| | importKind: 'value', |
| | }) |
| |
|
| | if (reactImportDeclaration.size() > 0) { |
| | const importNode = reactImportDeclaration.get().node |
| |
|
| | |
| | importNode.specifiers.push(j.importSpecifier(j.identifier('use'))) |
| | } else { |
| | |
| | if (reactImportDeclaration.size() > 0) { |
| | reactImportDeclaration |
| | .get() |
| | .node.specifiers.push(j.importSpecifier(j.identifier('use'))) |
| | } else { |
| | |
| | root |
| | .get() |
| | .node.program.body.unshift( |
| | j.importDeclaration( |
| | [j.importSpecifier(j.identifier('use'))], |
| | j.literal('react') |
| | ) |
| | ) |
| | } |
| | } |
| | } |
| | } |
| |
|
| | function findSubScopeArgumentIdentifier( |
| | path: ASTPath<any>, |
| | j: API['j'], |
| | argName: string |
| | ) { |
| | const defCount = j(path).find(j.Identifier, { name: argName }).size() |
| |
|
| | return defCount > 0 |
| | } |
| |
|
| | export function generateUniqueIdentifier( |
| | defaultIdName: string, |
| | path: ASTPath<any>, |
| | j: API['j'] |
| | ): ReturnType<typeof j.identifier> { |
| | let idName = defaultIdName |
| | let idNameSuffix = 0 |
| | while (findSubScopeArgumentIdentifier(path, j, idName)) { |
| | idName = defaultIdName + idNameSuffix |
| | idNameSuffix++ |
| | } |
| |
|
| | const propsIdentifier = j.identifier(idName) |
| | return propsIdentifier |
| | } |
| |
|
| | export function isFunctionScope( |
| | path: ASTPath, |
| | j: API['jscodeshift'] |
| | ): path is ASTPath<FunctionScope> { |
| | if (!path) return false |
| | const node = path.node |
| |
|
| | |
| | return ( |
| | j.FunctionDeclaration.check(node) || |
| | j.FunctionExpression.check(node) || |
| | j.ArrowFunctionExpression.check(node) |
| | ) |
| | } |
| |
|
| | export function findClosetParentFunctionScope( |
| | path: ASTPath, |
| | j: API['jscodeshift'] |
| | ) { |
| | if (!path.scope) return null |
| | let parentFunctionPath = path.scope.path |
| | while (parentFunctionPath && !isFunctionScope(parentFunctionPath, j)) { |
| | parentFunctionPath = parentFunctionPath.parent |
| | } |
| |
|
| | return parentFunctionPath |
| | } |
| |
|
| | function getFunctionNodeFromBinding( |
| | bindingPath: ASTPath<any>, |
| | idName: string, |
| | j: API['jscodeshift'], |
| | root: Collection<any> |
| | ): ASTPath<FunctionScope> | undefined { |
| | const bindingNode = bindingPath.node |
| | if ( |
| | j.FunctionDeclaration.check(bindingNode) || |
| | j.FunctionExpression.check(bindingNode) || |
| | j.ArrowFunctionExpression.check(bindingNode) |
| | ) { |
| | return bindingPath |
| | } else if (j.VariableDeclarator.check(bindingNode)) { |
| | const init = bindingNode.init |
| | |
| | if ( |
| | j.FunctionExpression.check(init) || |
| | j.ArrowFunctionExpression.check(init) |
| | ) { |
| | return bindingPath.get('init') |
| | } |
| | } else if (j.Identifier.check(bindingNode)) { |
| | const variablePath = root.find(j.VariableDeclaration, { |
| | |
| | declarations: [ |
| | { |
| | |
| | type: 'VariableDeclarator', |
| | |
| | id: { |
| | type: 'Identifier', |
| | name: idName, |
| | }, |
| | }, |
| | ], |
| | }) |
| |
|
| | if (variablePath.size()) { |
| | const variableDeclarator = variablePath.get()?.node?.declarations?.[0] |
| | if (j.VariableDeclarator.check(variableDeclarator)) { |
| | const init = variableDeclarator.init |
| | if ( |
| | j.FunctionExpression.check(init) || |
| | j.ArrowFunctionExpression.check(init) |
| | ) { |
| | return variablePath.get('declarations', 0, 'init') |
| | } |
| | } |
| | } |
| |
|
| | const functionDeclarations = root.find(j.FunctionDeclaration, { |
| | id: { |
| | name: idName, |
| | }, |
| | }) |
| | if (functionDeclarations.size()) { |
| | return functionDeclarations.get() |
| | } |
| | } |
| | return undefined |
| | } |
| |
|
| | export function getFunctionPathFromExportPath( |
| | exportPath: ASTPath<ExportDefaultDeclaration | ExportNamedDeclaration>, |
| | j: API['jscodeshift'], |
| | root: Collection<any>, |
| | namedExportFilter: (idName: string) => boolean |
| | ): ASTPath<FunctionScope> | undefined { |
| | |
| | if (j.ExportDefaultDeclaration.check(exportPath.node)) { |
| | const { declaration } = exportPath.node |
| | if (declaration) { |
| | if ( |
| | j.FunctionDeclaration.check(declaration) || |
| | j.FunctionExpression.check(declaration) || |
| | j.ArrowFunctionExpression.check(declaration) |
| | ) { |
| | return exportPath.get('declaration') |
| | } else if (j.Identifier.check(declaration)) { |
| | const idName = declaration.name |
| | if (!namedExportFilter(idName)) return |
| |
|
| | const exportBinding = exportPath.scope.getBindings()[idName]?.[0] |
| | if (exportBinding) { |
| | return getFunctionNodeFromBinding(exportBinding, idName, j, root) |
| | } |
| | } |
| | } |
| | } else if ( |
| | |
| | j.ExportNamedDeclaration.check(exportPath.node) |
| | ) { |
| | const namedExportPath = exportPath as ASTPath<ExportNamedDeclaration> |
| | |
| | const { declaration, specifiers } = namedExportPath.node |
| | if (declaration) { |
| | if (j.VariableDeclaration.check(declaration)) { |
| | const { declarations } = declaration |
| | for (const decl of declarations) { |
| | if (j.VariableDeclarator.check(decl) && j.Identifier.check(decl.id)) { |
| | const idName = decl.id.name |
| |
|
| | if (!namedExportFilter(idName)) return |
| |
|
| | |
| | const exportBinding = |
| | namedExportPath.scope.getBindings()[idName]?.[0] |
| | if (exportBinding) { |
| | return getFunctionNodeFromBinding(exportBinding, idName, j, root) |
| | } |
| | } |
| | } |
| | } else if ( |
| | j.FunctionDeclaration.check(declaration) || |
| | j.FunctionExpression.check(declaration) || |
| | j.ArrowFunctionExpression.check(declaration) |
| | ) { |
| | const funcName = declaration.id?.name |
| | if (!namedExportFilter(funcName)) return |
| |
|
| | return namedExportPath.get('declaration') |
| | } |
| | } |
| | if (specifiers) { |
| | for (const specifier of specifiers) { |
| | if (j.ExportSpecifier.check(specifier)) { |
| | const idName = specifier.local.name |
| |
|
| | if (!namedExportFilter(idName)) return |
| |
|
| | const exportBinding = namedExportPath.scope.getBindings()[idName]?.[0] |
| | if (exportBinding) { |
| | return getFunctionNodeFromBinding(exportBinding, idName, j, root) |
| | } |
| | } |
| | } |
| | } |
| | } |
| |
|
| | return undefined |
| | } |
| |
|
| | export function wrapParentheseIfNeeded( |
| | hasChainAccess: boolean, |
| | j: API['jscodeshift'], |
| | expression |
| | ) { |
| | return hasChainAccess ? j.parenthesizedExpression(expression) : expression |
| | } |
| |
|
| | function existsComment( |
| | comments: ASTPath<any>['node']['comments'], |
| | comment: string |
| | ): boolean { |
| | const isCodemodErrorComment = comment |
| | .trim() |
| | .startsWith(NEXT_CODEMOD_ERROR_PREFIX) |
| |
|
| | let hasIgnoreComment = false |
| | let hasComment = false |
| |
|
| | if (comments) { |
| | comments.forEach((commentNode) => { |
| | const currentComment = commentNode.value |
| | if (currentComment.trim().startsWith(NEXT_CODEMOD_IGNORE_ERROR_PREFIX)) { |
| | hasIgnoreComment = true |
| | } |
| | if (currentComment === comment) { |
| | hasComment = true |
| | } |
| | }) |
| | |
| | |
| | |
| | if (hasIgnoreComment && isCodemodErrorComment) { |
| | return true |
| | } |
| | if (hasComment) { |
| | return true |
| | } |
| | } |
| | return false |
| | } |
| |
|
| | export function insertCommentOnce( |
| | node: ASTPath<any>['node'], |
| | j: API['j'], |
| | comment: string |
| | ): boolean { |
| | const hasCommentInInlineComments = existsComment(node.comments, comment) |
| | const hasCommentInLeadingComments = existsComment( |
| | node.leadingComments, |
| | comment |
| | ) |
| |
|
| | if (!hasCommentInInlineComments && !hasCommentInLeadingComments) { |
| | |
| | node.comments = [j.commentBlock(comment), ...(node.comments || [])] |
| | return true |
| | } |
| |
|
| | return false |
| | } |
| |
|
| | export function getVariableDeclaratorId( |
| | path: ASTPath<any>, |
| | j: API['j'] |
| | ): ASTPath<any>['node']['id'] | undefined { |
| | const parent = path.parentPath |
| | if (j.VariableDeclarator.check(parent.node)) { |
| | const id = parent.node.id |
| | if (j.Identifier.check(id)) { |
| | return id |
| | } |
| | } |
| | return undefined |
| | } |
| |
|
| | export function findFunctionBody(path: ASTPath<FunctionScope>): null | any[] { |
| | let functionBody = path.node.body |
| | if (functionBody && functionBody.type === 'BlockStatement') { |
| | return functionBody.body |
| | } |
| | return null |
| | } |
| |
|
| | const isPascalCase = (s: string) => /^[A-Z][a-z0-9]*$/.test(s) |
| |
|
| | export const isReactHookName = (name: string) => |
| | |
| | name === 'use' || |
| | |
| | (name.startsWith('use') && name[3] === name[3].toUpperCase()) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export function containsReactHooksCallExpressions( |
| | path: ASTPath<FunctionScope>, |
| | j: API['jscodeshift'] |
| | ) { |
| | const hasReactHooks = |
| | j(path) |
| | .find(j.CallExpression) |
| | .filter((callPath) => { |
| | |
| | |
| | |
| | const isUseHookOrReactHookCall = |
| | j.Identifier.check(callPath.value.callee) && |
| | isReactHookName(callPath.value.callee.name) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | const isReactUseCall = |
| | j.MemberExpression.check(callPath.value.callee) && |
| | j.Identifier.check(callPath.value.callee.object) && |
| | j.Identifier.check(callPath.value.callee.property) && |
| | isPascalCase(callPath.value.callee.object.name) && |
| | isReactHookName(callPath.value.callee.property.name) |
| |
|
| | return isUseHookOrReactHookCall || isReactUseCall |
| | }) |
| | .size() > 0 |
| | return hasReactHooks |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | export function isParentUseCallExpression( |
| | path: ASTPath<any>, |
| | j: API['jscodeshift'] |
| | ) { |
| | const isParentUseCall = |
| | |
| | j.CallExpression.check(path.parent.value) && |
| | |
| | path.parent.value.arguments[0] === path.value && |
| | path.parent.value.arguments.length === 1 && |
| | |
| | j.Identifier.check(path.parent.value.callee) && |
| | path.parent.value.callee.name === 'use' |
| | const isParentReactUseCall = |
| | |
| | j.CallExpression.check(path.parent.value) && |
| | |
| | path.parent.value.arguments[0] === path.value && |
| | path.parent.value.arguments.length === 1 && |
| | |
| | j.MemberExpression.check(path.parent.value.callee) && |
| | j.Identifier.check(path.parent.value.callee.object) && |
| | path.parent.value.callee.object.name === 'React' && |
| | j.Identifier.check(path.parent.value.callee.property) && |
| | path.parent.value.callee.property.name === 'use' |
| | return isParentUseCall || isParentReactUseCall |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | export function isParentPromiseAllCallExpression( |
| | path: ASTPath<any>, |
| | j: API['jscodeshift'] |
| | ) { |
| | const argsParent = path.parent |
| | const callParent = argsParent?.parent |
| | if ( |
| | argsParent && |
| | callParent && |
| | j.ArrayExpression.check(argsParent.value) && |
| | j.CallExpression.check(callParent.value) && |
| | j.MemberExpression.check(callParent.value.callee) && |
| | j.Identifier.check(callParent.value.callee.object) && |
| | callParent.value.callee.object.name === 'Promise' && |
| | j.Identifier.check(callParent.value.callee.property) && |
| | callParent.value.callee.property.name === 'all' |
| | ) { |
| | return true |
| | } |
| | return false |
| | } |
| |
|