From 4900db402c9cbd6a5b4bdd8bf5a6a79c84a70218 Mon Sep 17 00:00:00 2001 From: Aleksy Rybicki <alekso.php@gmail.com> Date: Thu, 27 Jun 2024 11:31:34 +0100 Subject: [PATCH 1/4] migrating to fetch from axios using codemod axios/fetch --- apps/backend/src/plugins/authPlugin.ts | 226 ++-- apps/backend/src/publishHandler.ts | 22 +- apps/backend/src/services/GithubProvider.ts | 342 +++--- apps/backend/src/services/PostHogService.ts | 89 +- apps/cli/src/fileDownloadService.ts | 8 +- apps/modgpt/src/plugins/authPlugin.ts | 6 +- apps/task-manager/src/services/Auth.ts | 80 +- apps/task-manager/src/util.ts | 77 +- .../src/components/webview/MainProvider.ts | 1053 +++++++++-------- .../fetch/__testfixtures__/fixture1.input.ts | 6 +- .../fetch/__testfixtures__/fixture2.input.ts | 35 +- pnpm-lock.yaml | 32 +- 12 files changed, 1020 insertions(+), 956 deletions(-) diff --git a/apps/backend/src/plugins/authPlugin.ts b/apps/backend/src/plugins/authPlugin.ts index 8561b9017..9d0fe3a83 100644 --- a/apps/backend/src/plugins/authPlugin.ts +++ b/apps/backend/src/plugins/authPlugin.ts @@ -1,128 +1,124 @@ -import type { OrganizationMembership, User } from "@codemod-com/utilities"; -import axios from "axios"; -import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; -import fp from "fastify-plugin"; -import { environment } from "../util"; +import type { OrganizationMembership, User } from '@codemod-com/utilities'; +import axios from 'axios'; +import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; +import fp from 'fastify-plugin'; +import { environment } from '../util'; export interface UserDataPopulatedRequest extends FastifyRequest { - user?: User; - organizations?: OrganizationMembership[]; - allowedNamespaces?: string[]; + user?: User; + organizations?: OrganizationMembership[]; + allowedNamespaces?: string[]; } export interface OAuthTokenPopulatedRequest extends FastifyRequest { - token?: string; + token?: string; } -declare module "fastify" { - interface FastifyInstance { - authenticate: ( - request: FastifyRequest, - reply: FastifyReply, - ) => Promise<void>; - getUserData: ( - request: FastifyRequest & UserDataPopulatedRequest, - reply: FastifyReply, - ) => Promise<void>; - getOAuthToken: ( - request: FastifyRequest & OAuthTokenPopulatedRequest, - reply: FastifyReply, - ) => Promise<void>; - } +declare module 'fastify' { + interface FastifyInstance { + authenticate: ( + request: FastifyRequest, + reply: FastifyReply, + ) => Promise<void>; + getUserData: ( + request: FastifyRequest & UserDataPopulatedRequest, + reply: FastifyReply, + ) => Promise<void>; + getOAuthToken: ( + request: FastifyRequest & OAuthTokenPopulatedRequest, + reply: FastifyReply, + ) => Promise<void>; + } } async function authPlugin(fastify: FastifyInstance, _opts: unknown) { - fastify.decorate( - "authenticate", - async (request: FastifyRequest, reply: FastifyReply) => { - try { - const authHeader = request.headers.authorization; - - if (!authHeader) reply.code(401).send({ error: "Unauthorized" }); - - await axios.get(`${environment.AUTH_SERVICE_URL}/verifyToken`, { - headers: { - Authorization: authHeader, - }, - }); - } catch (error) { - console.error(error); - reply.code(401).send({ error: "Unauthorized" }); - } - }, - ); - - fastify.decorate( - "getUserData", - async ( - request: FastifyRequest & { - user?: User; - organizations?: OrganizationMembership[]; - allowedNamespaces?: string[]; - }, - reply: FastifyReply, - ) => { - try { - const authHeader = request.headers.authorization; - - if (!authHeader) { - request.user = undefined; - request.organizations = undefined; - request.allowedNamespaces = undefined; - return; - } - - const { data } = await axios.get( - `${environment.AUTH_SERVICE_URL}/userData`, - { - headers: { - Authorization: authHeader, - }, - }, - ); - - const { user, organizations, allowedNamespaces } = data; - - request.user = user; - request.organizations = organizations; - request.allowedNamespaces = allowedNamespaces; - } catch (error) { - console.error(error); - reply.code(401).send({ error: "Unauthorized" }); - } - }, - ); - - fastify.decorate( - "getOAuthToken", - async ( - request: FastifyRequest & { - token?: string; - }, - reply: FastifyReply, - ) => { - try { - const authHeader = request.headers.authorization; - - if (!authHeader) reply.code(401).send({ error: "Unauthorized" }); - - const { data } = await axios.get( - `${environment.AUTH_SERVICE_URL}/oAuthToken`, - { - headers: { - Authorization: authHeader, - }, - }, - ); - - const { token } = data; - - request.token = token; - } catch { - reply.code(401).send({ error: "Unauthorized" }); - } - }, - ); + fastify.decorate( + 'authenticate', + async (request: FastifyRequest, reply: FastifyReply) => { + try { + const authHeader = request.headers.authorization; + + if (!authHeader) + reply.code(401).send({ error: 'Unauthorized' }); + + await fetch(`${environment.AUTH_SERVICE_URL}/verifyToken`, { + headers: { Authorization: authHeader }, + }); + } catch (error) { + console.error(error); + reply.code(401).send({ error: 'Unauthorized' }); + } + }, + ); + + fastify.decorate( + 'getUserData', + async ( + request: FastifyRequest & { + user?: User; + organizations?: OrganizationMembership[]; + allowedNamespaces?: string[]; + }, + reply: FastifyReply, + ) => { + try { + const authHeader = request.headers.authorization; + + if (!authHeader) { + request.user = undefined; + request.organizations = undefined; + request.allowedNamespaces = undefined; + return; + } + + const response = await fetch( + `${environment.AUTH_SERVICE_URL}/userData`, + { headers: { Authorization: authHeader } }, + ); + if (!response.ok) throw new Error('Failed to fetch userData'); + const { data } = { data: await response.json() }; + + const { user, organizations, allowedNamespaces } = data; + + request.user = user; + request.organizations = organizations; + request.allowedNamespaces = allowedNamespaces; + } catch (error) { + console.error(error); + reply.code(401).send({ error: 'Unauthorized' }); + } + }, + ); + + fastify.decorate( + 'getOAuthToken', + async ( + request: FastifyRequest & { + token?: string; + }, + reply: FastifyReply, + ) => { + try { + const authHeader = request.headers.authorization; + + if (!authHeader) + reply.code(401).send({ error: 'Unauthorized' }); + + const response = await fetch( + `${environment.AUTH_SERVICE_URL}/oAuthToken`, + { headers: { Authorization: authHeader } }, + ); + if (!response.ok) throw new Error('Failed to fetch oAuthToken'); + const { data } = { data: await response.json() }; + + const { token } = data; + + request.token = token; + } catch { + reply.code(401).send({ error: 'Unauthorized' }); + } + }, + ); } export default fp(authPlugin); diff --git a/apps/backend/src/publishHandler.ts b/apps/backend/src/publishHandler.ts index 410709ed7..1982b81c5 100644 --- a/apps/backend/src/publishHandler.ts +++ b/apps/backend/src/publishHandler.ts @@ -350,27 +350,7 @@ export const publishHandler: RouteHandler<{ if (latestVersion === null) { try { - await axios.post( - "https://hooks.zapier.com/hooks/catch/18983913/2ybuovt/", - { - codemod: { - name, - from: codemodRc.applicability?.from?.map((tuple) => - tuple.join(" "), - ), - to: codemodRc.applicability?.to?.map((tuple) => tuple.join(" ")), - engine: codemodRc.engine, - publishedAt: createdAtTimestamp, - }, - author: { - username, - name: `${firstName ?? ""} ${lastName ?? ""}`.trim() || null, - email: - emailAddresses.find((e) => e.id === primaryEmailAddressId) - ?.emailAddress ?? null, - }, - }, - ); + await const controller = new AbortController();const signal = controller.signal;setTimeout(() => controller.abort(), 5000);try { const response = await fetch( "https://hooks.zapier.com/hooks/catch/18983913/2ybuovt/", { method: 'POST', body: JSON.stringify({ codemod: { name, from: codemodRc.applicability?.from?.map((tuple) => tuple.join(" ")), to: codemodRc.applicability?.to?.map((tuple) => tuple.join(" ")), engine: codemodRc.engine, publishedAt: createdAtTimestamp, }, author: { username, name: `${firstName ?? ""} ${lastName ?? ""}`.trim() || null, email: emailAddresses.find((e) => e.id === primaryEmailAddressId)?.emailAddress ?? null, }, }), headers: { 'Content-Type': 'application/json' }, signal: signal } ); if (!response.ok) { throw new Error('Network response was not ok'); } const result = { data: await response.json() };} catch (err) { console.error("Failed calling Zapier hook:", err);}; } catch (err) { console.error("Failed calling Zapier hook:", err); } diff --git a/apps/backend/src/services/GithubProvider.ts b/apps/backend/src/services/GithubProvider.ts index 64af7ebe5..c886c389c 100644 --- a/apps/backend/src/services/GithubProvider.ts +++ b/apps/backend/src/services/GithubProvider.ts @@ -1,185 +1,195 @@ -import axios, { type AxiosResponse } from "axios"; -import gh from "parse-github-url"; +import axios, { type AxiosResponse } from 'axios'; +import gh from 'parse-github-url'; import type { - Assignee, - CreatePRParams, - GHBranch, - GithubContent, - GithubRepository, - Issue, - ListPRParams, - NewIssueParams, - PullRequest, - SourceControlProvider, -} from "./SourceControl.js"; + Assignee, + CreatePRParams, + GHBranch, + GithubContent, + GithubRepository, + Issue, + ListPRParams, + NewIssueParams, + PullRequest, + SourceControlProvider, +} from './SourceControl.js'; type Repository = { - owner: string; - name: string; + owner: string; + name: string; }; class InvalidGithubUrlError extends Error {} class ParseGithubUrlError extends Error {} function parseGithubRepoUrl(url: string): Repository { - try { - const { owner, name } = gh(url) ?? {}; - - if (!owner || !name) { - throw new InvalidGithubUrlError("Missing owner or name"); - } - - return { owner, name }; - } catch (e) { - if (e instanceof InvalidGithubUrlError) { - throw e; - } - - const errorMessage = e instanceof Error ? e.message : String(e); - throw new ParseGithubUrlError(errorMessage); - } + try { + const { owner, name } = gh(url) ?? {}; + + if (!owner || !name) { + throw new InvalidGithubUrlError('Missing owner or name'); + } + + return { owner, name }; + } catch (e) { + if (e instanceof InvalidGithubUrlError) { + throw e; + } + + const errorMessage = e instanceof Error ? e.message : String(e); + throw new ParseGithubUrlError(errorMessage); + } } const withPagination = async ( - paginatedRequest: (page: string) => Promise<AxiosResponse<any[]>>, + paginatedRequest: (page: string) => Promise<AxiosResponse<any[]>>, ) => { - const nextPattern = /(?<=<)([\S]*)(?=>; rel="Next")/i; - let nextPage: string | null = "1"; - let data: any[] = []; - - while (nextPage !== null) { - const response = await paginatedRequest(nextPage); - data = [...data, ...(response.data ?? [])]; - - const linkHeader = response.headers.link; - - if (typeof linkHeader === "string" && linkHeader.includes(`rel=\"next\"`)) { - const nextUrl = linkHeader.match(nextPattern)?.[0]; - nextPage = nextUrl ? new URL(nextUrl).searchParams.get("page") : null; - } else { - nextPage = null; - } - } - - return data; + const nextPattern = /(?<=<)([\S]*)(?=>; rel="Next")/i; + let nextPage: string | null = '1'; + let data: any[] = []; + + while (nextPage !== null) { + const response = await paginatedRequest(nextPage); + data = [...data, ...(response.data ?? [])]; + + const linkHeader = response.headers.link; + + if ( + typeof linkHeader === 'string' && + linkHeader.includes(`rel=\"next\"`) + ) { + const nextUrl = linkHeader.match(nextPattern)?.[0]; + nextPage = nextUrl + ? new URL(nextUrl).searchParams.get('page') + : null; + } else { + nextPage = null; + } + } + + return data; }; const PER_PAGE = 99; export class GithubProvider implements SourceControlProvider { - private readonly __repo: string | null = null; - private readonly __baseUrl: string; - private readonly __authHeader: string; - - constructor(oAuthToken: string, repoUrl: string | null) { - this.__baseUrl = "https://api.github.com"; - this.__repo = repoUrl; - this.__authHeader = `Bearer ${oAuthToken}`; - } - - private get __repoUrl() { - const { owner, name } = parseGithubRepoUrl(this.__repo ?? ""); - - return `${this.__baseUrl}/repos/${owner}/${name}`; - } - - async createIssue(params: NewIssueParams): Promise<Issue> { - const res = await axios.post(`${this.__repoUrl}/issues`, params, { - headers: { - Authorization: this.__authHeader, - }, - }); - - return res.data; - } - - async createPullRequest(params: CreatePRParams): Promise<PullRequest> { - const res = await axios.post(`${this.__repoUrl}/pulls`, params, { - headers: { - Authorization: this.__authHeader, - }, - }); - - return res.data; - } - - async getPullRequests(params: ListPRParams): Promise<PullRequest[]> { - const queryParams = Object.entries(params).reduce<Record<string, string>>( - (acc, [key, value]) => { - if (value) { - acc[key] = value; - } - - return acc; - }, - {}, - ); - - const query = new URLSearchParams(queryParams).toString(); - - const res = await axios.get(`${this.__repoUrl}/pulls?${query}`, { - headers: { - Authorization: this.__authHeader, - }, - }); - - return res.data; - } - - async getAssignees(): Promise<Assignee[]> { - const res = await axios.get(`${this.__repoUrl}/assignees`, { - headers: { - Authorization: this.__authHeader, - }, - }); - - return res.data; - } - - private __getUserRepositories = async ( - page: string, - ): Promise<AxiosResponse<GithubRepository[]>> => { - return await axios.get<GithubRepository[]>( - `https://api.github.com/user/repos?per_page=${PER_PAGE}&page=${page}`, - { - headers: { - Authorization: this.__authHeader, - }, - }, - ); - }; - - async getUserRepositories(): Promise<GithubRepository[]> { - return await withPagination(this.__getUserRepositories); - } - - private __getBranches = async ( - page: string, - ): Promise<AxiosResponse<GHBranch[]>> => { - return await axios.get( - `${this.__repoUrl}/branches?per_page=${PER_PAGE}&page=${page}`, - { - headers: { - Authorization: this.__authHeader, - }, - }, - ); - }; - - async getBranches(): Promise<string[]> { - return await withPagination(this.__getBranches); - } - - async getRepoContents(branchName: string): Promise<GithubContent[]> { - const res = await axios.get( - `${this.__repoUrl}/contents?ref=${branchName}`, - { - headers: { - Authorization: this.__authHeader, - }, - }, - ); - - return res.data; - } + private readonly __repo: string | null = null; + private readonly __baseUrl: string; + private readonly __authHeader: string; + + constructor(oAuthToken: string, repoUrl: string | null) { + this.__baseUrl = 'https://api.github.com'; + this.__repo = repoUrl; + this.__authHeader = `Bearer ${oAuthToken}`; + } + + private get __repoUrl() { + const { owner, name } = parseGithubRepoUrl(this.__repo ?? ''); + + return `${this.__baseUrl}/repos/${owner}/${name}`; + } + + async createIssue(params: NewIssueParams): Promise<Issue> { + const response = await fetch(`${this.__repoUrl}/issues`, { + method: 'POST', + headers: { + Authorization: this.__authHeader, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + if (!response.ok) throw new Error('Network response was not ok'); + const res = { data: (await response.json()) as Issue }; + + return res.data; + } + + async createPullRequest(params: CreatePRParams): Promise<PullRequest> { + const response = await fetch(`${this.__repoUrl}/pulls`, { + method: 'POST', + headers: { + Authorization: this.__authHeader, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + if (!response.ok) throw new Error('Network response was not ok'); + const res = { data: (await response.json()) as PullRequest }; + + return res.data; + } + + async getPullRequests(params: ListPRParams): Promise<PullRequest[]> { + const queryParams = Object.entries(params).reduce< + Record<string, string> + >((acc, [key, value]) => { + if (value) { + acc[key] = value; + } + + return acc; + }, {}); + + const query = new URLSearchParams(queryParams).toString(); + + const response = await fetch(`${this.__repoUrl}/pulls?${query}`, { + headers: { Authorization: this.__authHeader }, + }); + if (!response.ok) throw new Error('Network response was not ok'); + const res = { data: (await response.json()) as PullRequest[] }; + + return res.data; + } + + async getAssignees(): Promise<Assignee[]> { + const response = await fetch(`${this.__repoUrl}/assignees`, { + headers: { Authorization: this.__authHeader }, + }); + if (!response.ok) throw new Error('Network response was not ok'); + const res = { data: (await response.json()) as Assignee[] }; + + return res.data; + } + + private __getUserRepositories = async ( + page: string, + ): Promise<AxiosResponse<GithubRepository[]>> => { + return await axios.get<GithubRepository[]>( + `https://api.github.com/user/repos?per_page=${PER_PAGE}&page=${page}`, + { + headers: { + Authorization: this.__authHeader, + }, + }, + ); + }; + + async getUserRepositories(): Promise<GithubRepository[]> { + return await withPagination(this.__getUserRepositories); + } + + private __getBranches = async ( + page: string, + ): Promise<AxiosResponse<GHBranch[]>> => { + const response = await fetch( + `${this.__repoUrl}/branches?per_page=${PER_PAGE}&page=${page}`, + { headers: { Authorization: this.__authHeader } }, + ); + if (!response.ok) throw new Error('Network response was not ok'); + return { data: (await response.json()) as GHBranch[] }; + }; + + async getBranches(): Promise<string[]> { + return await withPagination(this.__getBranches); + } + + async getRepoContents(branchName: string): Promise<GithubContent[]> { + const response = await fetch( + `${this.__repoUrl}/contents?ref=${branchName}`, + { headers: { Authorization: this.__authHeader } }, + ); + if (!response.ok) throw new Error('Network response was not ok'); + const res = { data: (await response.json()) as GithubContent[] }; + + return res.data; + } } diff --git a/apps/backend/src/services/PostHogService.ts b/apps/backend/src/services/PostHogService.ts index 90970916c..ff3c06d62 100644 --- a/apps/backend/src/services/PostHogService.ts +++ b/apps/backend/src/services/PostHogService.ts @@ -1,50 +1,59 @@ -import { buildCodemodSlug } from "@codemod-com/utilities"; -import axios, { isAxiosError } from "axios"; +import { buildCodemodSlug } from '@codemod-com/utilities'; +import axios, { isAxiosError } from 'axios'; export class PostHogCodemodNotFoundError extends Error {} export class PostHogService { - private readonly __authHeader: string; - private readonly __projectId: string; + private readonly __authHeader: string; + private readonly __projectId: string; - constructor(authKey: string, projectId: string) { - this.__authHeader = `Bearer ${authKey}`; - this.__projectId = projectId; - } + constructor(authKey: string, projectId: string) { + this.__authHeader = `Bearer ${authKey}`; + this.__projectId = projectId; + } - async getCodemodTotalRuns(): Promise<Array<{ slug: string; runs: number }>> { - try { - const { data } = await axios.post( - `https://app.posthog.com/api/projects/${this.__projectId}/query/`, - { - query: { - kind: "HogQLQuery", - query: - "select properties.codemodName, count(*) from events where event in ('codemod.CLI.codemodExecuted', 'codemod.VSCE.codemodExecuted') group by properties.codemodName limit 500", - }, - }, - { - headers: { - Authorization: this.__authHeader, - }, - }, - ); + async getCodemodTotalRuns(): Promise< + Array<{ slug: string; runs: number }> + > { + try { + const response = await fetch( + `https://app.posthog.com/api/projects/${this.__projectId}/query/`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: this.__authHeader, + }, + body: JSON.stringify({ + query: { + kind: 'HogQLQuery', + query: "select properties.codemodName, count(*) from events where event in ('codemod.CLI.codemodExecuted', 'codemod.VSCE.codemodExecuted') group by properties.codemodName limit 500", + }, + }), + }, + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); - const result = data?.results?.map((value: [string, number]) => ({ - // @TODO add isLocal field to telemetry event, exclude local events from total runs - slug: buildCodemodSlug(value[0].replaceAll(" (from user machine)", "")), - runs: value[1], - })); + const result = data?.results?.map((value: [string, number]) => ({ + // @TODO add isLocal field to telemetry event, exclude local events from total runs + slug: buildCodemodSlug( + value[0].replaceAll(' (from user machine)', ''), + ), + runs: value[1], + })); - return result; - } catch (error) { - const errorMessage = isAxiosError<{ message: string }>(error) - ? error.response?.data.message - : (error as Error).message; + return result; + } catch (error) { + const errorMessage = isAxiosError<{ message: string }>(error) + ? error.response?.data.message + : (error as Error).message; - throw new PostHogCodemodNotFoundError( - `Failed to retrieve events. Reason: ${errorMessage}`, - ); - } - } + throw new PostHogCodemodNotFoundError( + `Failed to retrieve events. Reason: ${errorMessage}`, + ); + } + } } diff --git a/apps/cli/src/fileDownloadService.ts b/apps/cli/src/fileDownloadService.ts index 59d7adf9d..70d885381 100644 --- a/apps/cli/src/fileDownloadService.ts +++ b/apps/cli/src/fileDownloadService.ts @@ -43,9 +43,7 @@ export class FileDownloadService implements FileDownloadServiceBlueprint { } } - const { data } = await axios.get(url, { - responseType: "arraybuffer", - }); + const response = await fetch(url);if (!response.ok) throw new Error('Network response was not ok.');const data = await response.arrayBuffer(); const buffer = Buffer.from(data); @@ -70,9 +68,7 @@ export class FileDownloadService implements FileDownloadServiceBlueprint { let response: AxiosResponse; try { - response = await axios.head(url, { - timeout: 15000, - }); + response = await fetch(url, { signal: AbortSignal.timeout(15000) })if (!response.ok) throw new Error('Network response was not ok.');; } catch (error) { if (!isAxiosError(error)) { throw error; diff --git a/apps/modgpt/src/plugins/authPlugin.ts b/apps/modgpt/src/plugins/authPlugin.ts index f81cd4c98..aa05eb427 100644 --- a/apps/modgpt/src/plugins/authPlugin.ts +++ b/apps/modgpt/src/plugins/authPlugin.ts @@ -32,11 +32,7 @@ async function authPlugin(fastify: FastifyInstance, _opts: unknown) { if (!authHeader) reply.code(401).send({ error: "Unauthorized" }); - await axios.get(`${environment.AUTH_SERVICE_URL}/verifyToken`, { - headers: { - Authorization: authHeader, - }, - }); + await const controller = new AbortController();setTimeout(() => controller.abort(), 5000); // Assuming a default timeout of 5000ms as it was not specified in the original axios callconst response = await fetch(`${environment.AUTH_SERVICE_URL}/verifyToken`, { headers: { Authorization: authHeader, }, signal: controller.signal});if (!response.ok) throw new Error('Failed to fetch');const result = { data: await response.json() };; } catch (error) { console.log(error); } diff --git a/apps/task-manager/src/services/Auth.ts b/apps/task-manager/src/services/Auth.ts index 1a91a7e79..12020be75 100644 --- a/apps/task-manager/src/services/Auth.ts +++ b/apps/task-manager/src/services/Auth.ts @@ -1,47 +1,47 @@ -import axios, { isAxiosError } from "axios"; +import axios, { isAxiosError } from 'axios'; export class AuthError extends Error {} const USER_ID_REGEX = /^[a-z0-9_]+$/i; export class AuthService { - private readonly __authHeader: string; - - constructor(authKey: string) { - if (!authKey) { - throw new AuthError("Invalid auth key provided."); - } - this.__authHeader = `Bearer ${authKey}`; - } - - async getAuthToken(userId: string): Promise<string> { - try { - if (!USER_ID_REGEX.test(userId)) { - throw new AuthError("Invalid userId."); - } - - const result = await axios.get( - `https://api.clerk.dev/v1/users/${userId}/oauth_access_tokens/github`, - { - headers: { - Authorization: this.__authHeader, - }, - }, - ); - - const token = result.data[0]?.token; - - if (!token) { - throw new AuthError("Missing OAuth token"); - } - - return token; - } catch (error) { - const { message } = error as Error; - - throw new AuthError( - `Failed to retrieve OAuth token for GitHub. Reason: ${message}`, - ); - } - } + private readonly __authHeader: string; + + constructor(authKey: string) { + if (!authKey) { + throw new AuthError('Invalid auth key provided.'); + } + this.__authHeader = `Bearer ${authKey}`; + } + + async getAuthToken(userId: string): Promise<string> { + try { + if (!USER_ID_REGEX.test(userId)) { + throw new AuthError('Invalid userId.'); + } + + const response = await fetch( + `https://api.clerk.dev/v1/users/${userId}/oauth_access_tokens/github`, + { headers: { Authorization: this.__authHeader } }, + ); + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + const result = { data: await response.json() }; + + const token = result.data[0]?.token; + + if (!token) { + throw new AuthError('Missing OAuth token'); + } + + return token; + } catch (error) { + const { message } = error as Error; + + throw new AuthError( + `Failed to retrieve OAuth token for GitHub. Reason: ${message}`, + ); + } + } } diff --git a/apps/task-manager/src/util.ts b/apps/task-manager/src/util.ts index 80fe2df47..1135c7495 100644 --- a/apps/task-manager/src/util.ts +++ b/apps/task-manager/src/util.ts @@ -1,7 +1,7 @@ -import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"; -import gh from "parse-github-url"; +import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'; +import gh from 'parse-github-url'; -import { parseEnvironment } from "./schemata/env.js"; +import { parseEnvironment } from './schemata/env.js'; export const environment = parseEnvironment(process.env); @@ -10,40 +10,53 @@ class ParseGithubUrlError extends Error {} class AxiosRequestError extends Error {} type Repository = { - authorName: string; - repoName: string; + authorName: string; + repoName: string; }; export function parseGithubRepoUrl(url: string): Repository { - try { - const { owner, name } = gh(url) ?? {}; - if (!owner || !name) - throw new InvalidGithubUrlError("Missing owner or name"); - - return { authorName: owner, repoName: name }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - throw new ParseGithubUrlError(errorMessage); - } + try { + const { owner, name } = gh(url) ?? {}; + if (!owner || !name) + throw new InvalidGithubUrlError('Missing owner or name'); + + return { authorName: owner, repoName: name }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + throw new ParseGithubUrlError(errorMessage); + } } export async function axiosRequest<T>( - url: string, - method: "get" | "post" | "put" | "delete", - data: Record<string, unknown> | null, - headers?: Record<string, string>, + url: string, + method: 'get' | 'post' | 'put' | 'delete', + data: Record<string, unknown> | null, + headers?: Record<string, string>, ): Promise<T> { - try { - const config: AxiosRequestConfig = { - url, - method, - data, - headers, - }; - - const res: AxiosResponse<T> = await axios(config); - return res.data; - } catch (error) { - throw new AxiosRequestError(); - } + try { + const config: AxiosRequestConfig = { + url, + method, + data, + headers, + }; + + const res: AxiosResponse<T> = await fetch(config.url, { + method: config.method, + headers: config.headers, + body: JSON.stringify(config.data), + signal: config.timeout + ? AbortSignal.timeout(config.timeout) + : undefined, + }).then((response) => { + if (!response.ok) { + throw new AxiosRequestError(); + } + return response.json().then((data) => ({ data })); + }); + return res.data; + } catch (error) { + throw new AxiosRequestError(); + } } diff --git a/apps/vsce/src/components/webview/MainProvider.ts b/apps/vsce/src/components/webview/MainProvider.ts index 92f30c2c9..fba6eaf35 100644 --- a/apps/vsce/src/components/webview/MainProvider.ts +++ b/apps/vsce/src/components/webview/MainProvider.ts @@ -1,522 +1,559 @@ -import axios from "axios"; -import areEqual from "fast-deep-equal"; -import { glob } from "glob"; +import axios from 'axios'; +import areEqual from 'fast-deep-equal'; +import { glob } from 'glob'; import { - type ExtensionContext, - Uri, - type WebviewView, - type WebviewViewProvider, - commands, - window, - workspace, -} from "vscode"; -import type { Store } from "../../data"; -import { actions } from "../../data/slice"; -import { createIssueResponseCodec } from "../../github/types"; + type ExtensionContext, + Uri, + type WebviewView, + type WebviewViewProvider, + commands, + window, + workspace, +} from 'vscode'; +import type { Store } from '../../data'; +import { actions } from '../../data/slice'; +import { createIssueResponseCodec } from '../../github/types'; import { - type CodemodNodeHashDigest, - relativeToAbsolutePath, - selectCodemodArguments, -} from "../../selectors/selectCodemodTree"; -import { selectMainWebviewViewProps } from "../../selectors/selectMainWebviewViewProps"; -import { buildGlobPattern, isNeitherNullNorUndefined } from "../../utilities"; -import type { EngineService } from "../engineService"; -import { type MessageBus, MessageKind } from "../messageBus"; -import { WebviewResolver } from "./WebviewResolver"; + type CodemodNodeHashDigest, + relativeToAbsolutePath, + selectCodemodArguments, +} from '../../selectors/selectCodemodTree'; +import { selectMainWebviewViewProps } from '../../selectors/selectMainWebviewViewProps'; +import { buildGlobPattern, isNeitherNullNorUndefined } from '../../utilities'; +import type { EngineService } from '../engineService'; +import { type MessageBus, MessageKind } from '../messageBus'; +import { WebviewResolver } from './WebviewResolver'; import type { - CodemodHash, - WebviewMessage, - WebviewResponse, -} from "./webviewEvents"; + CodemodHash, + WebviewMessage, + WebviewResponse, +} from './webviewEvents'; export const validateAccessToken = async ( - accessToken: string, + accessToken: string, ): Promise<void> => { - try { - const response = await axios.post( - "https://backend.codemod.com/verifyToken", - {}, - { - headers: { Authorization: `Bearer ${accessToken}` }, - timeout: 5000, - }, - ); - - return response.data; - } catch (error) { - if (!axios.isAxiosError(error)) { - console.error(error); - } - } + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + const response = await fetch( + 'https://backend.codemod.com/verifyToken', + { + method: 'POST', + headers: { Authorization: `Bearer ${accessToken}` }, + signal: controller.signal, + }, + ); + clearTimeout(timeoutId); + if (!response.ok) throw new Error('Network response was not ok.'); + const data = await response.json(); + + return response.data; + } catch (error) { + if ((!error) instanceof Error && error.name === 'AbortError') { + console.error(error); + } + } }; export const createIssue = async ( - title: string, - body: string, - accessToken: string, - onSuccess: () => void, - onFail: () => Promise<void>, + title: string, + body: string, + accessToken: string, + onSuccess: () => void, + onFail: () => Promise<void>, ): Promise<{ status: number; html_url: string | null }> => { - // call API to create Github Issue - const codemodRegistryRepoUrl = "https://github.com/codemod-com/codemod"; - - const result = await axios.post( - "https://backend.codemod.com/sourceControl/github/issues", - { title, body, repoUrl: codemodRegistryRepoUrl }, - { headers: { Authorization: `Bearer ${accessToken}` } }, - ); - if (result.status !== 200) { - await onFail(); - return { status: result.status, html_url: null }; - } - - const { data } = result; - - const validation = createIssueResponseCodec.decode(data); - - if (validation._tag === "Left") { - await onFail(); - window.showErrorMessage("Creating Github issue failed."); - return { status: 406, html_url: null }; - } - - onSuccess(); - - const decision = await window.showInformationMessage( - "Github issue is successfully created.", - "See issue in Github", - ); - const { html_url } = validation.right; - if (decision === "See issue in Github") { - commands.executeCommand("codemod.redirect", html_url); - } - return { - status: 200, - html_url, - }; + // call API to create Github Issue + const codemodRegistryRepoUrl = 'https://github.com/codemod-com/codemod'; + + const result = await fetch( + 'https://backend.codemod.com/sourceControl/github/issues', + { + method: 'POST', + headers: { Authorization: `Bearer ${accessToken}` }, + body: JSON.stringify({ + title, + body, + repoUrl: codemodRegistryRepoUrl, + }), + }, + ); + if (!result.ok) throw new Error('Network response was not ok.'); + const data = await result.json(); + if (result.status !== 200) { + await onFail(); + return { status: result.status, html_url: null }; + } + + const { data } = result; + + const validation = createIssueResponseCodec.decode(data); + + if (validation._tag === 'Left') { + await onFail(); + window.showErrorMessage('Creating Github issue failed.'); + return { status: 406, html_url: null }; + } + + onSuccess(); + + const decision = await window.showInformationMessage( + 'Github issue is successfully created.', + 'See issue in Github', + ); + const { html_url } = validation.right; + if (decision === 'See issue in Github') { + commands.executeCommand('codemod.redirect', html_url); + } + return { + status: 200, + html_url, + }; }; export class MainViewProvider implements WebviewViewProvider { - private __view: WebviewView | null = null; - private __webviewResolver: WebviewResolver; - private __executionQueue: ReadonlyArray<CodemodHash> = []; - private __directoryPaths: ReadonlyArray<string> | null = null; - // true by default to prevent banner blinking on load - private __codemodEngineNodeLocated = true; - - constructor( - context: ExtensionContext, - private readonly __engineService: EngineService, - private readonly __messageBus: MessageBus, - private readonly __rootUri: Uri | null, - private readonly __store: Store, - ) { - this.__webviewResolver = new WebviewResolver(context.extensionUri); - - this.__messageBus.subscribe(MessageKind.showProgress, (message) => { - if (message.codemodHash === null) { - return; - } - - this.__postMessage({ - kind: "webview.global.setCodemodExecutionProgress", - codemodHash: message.codemodHash, - progressKind: message.progressKind, - totalFileNumber: message.totalFileNumber, - processedFileNumber: message.processedFileNumber, - }); - }); - - this.__messageBus.subscribe(MessageKind.codemodSetExecuted, () => { - this.__postMessage({ - kind: "webview.global.codemodExecutionHalted", - }); - }); - - this.__messageBus.subscribe(MessageKind.executeCodemodSet, () => { - this.__store.dispatch(actions.collapseResultsPanel(false)); - this.__store.dispatch(actions.collapseChangeExplorerPanel(false)); - }); - - this.__messageBus.subscribe(MessageKind.executionQueueChange, (message) => { - this.__executionQueue = message.queuedCodemodHashes; - const props = this.__buildProps(); - - this.__postMessage({ - kind: "webview.main.setProps", - props: props, - }); - }); - - this.__messageBus.subscribe( - MessageKind.codemodEngineNodeLocated, - ({ codemodEngineNodeLocated }) => { - if (this.__codemodEngineNodeLocated === codemodEngineNodeLocated) { - return; - } - - this.__codemodEngineNodeLocated = codemodEngineNodeLocated; - - const props = this.__buildProps(); - - this.__postMessage({ - kind: "webview.main.setProps", - props, - }); - }, - ); - - let prevProps = this.__buildProps(); - - this.__store.subscribe(async () => { - if (this.__directoryPaths === null) { - await this.__getDirectoryPaths(); - } - - const nextProps = this.__buildProps(); - if (areEqual(prevProps, nextProps)) { - return; - } - - prevProps = nextProps; - - this.__postMessage({ - kind: "webview.main.setProps", - props: nextProps, - }); - }); - } - - public isVisible(): boolean { - return this.__view?.visible ?? false; - } - - public resolveWebviewView(webviewView: WebviewView): void | Thenable<void> { - this.__resolveWebview(webviewView); - - this.__view = webviewView; - - this.__view.webview.onDidReceiveMessage(this.__onDidReceiveMessage); - - this.__messageBus.publish({ - kind: MessageKind.mainWebviewViewVisibilityChange, - }); - - this.__view.onDidChangeVisibility(() => { - this.__messageBus.publish({ - kind: MessageKind.mainWebviewViewVisibilityChange, - }); - - if (this.__view?.visible) { - this.__resolveWebview(this.__view); - } - }); - } - - private async __getDirectoryPaths() { - if (this.__rootUri === null) { - return; - } - - const globPattern = buildGlobPattern(this.__rootUri, "/**"); - - // From `glob` documentation: - // (Note: to match only directories, put a / at the end of the pattern.) - const directoryPaths = await glob(`${globPattern}/`, { - // ignore node_modules and files, match only directories - ignore: ["**/node_modules/**"], - follow: false, - maxDepth: 10, - }); - - const MAX_NUMBER_OF_DIRECTORIES = 10000; - - this.__directoryPaths = directoryPaths.slice(0, MAX_NUMBER_OF_DIRECTORIES); - } - - private __postMessage(message: WebviewMessage) { - this.__view?.webview.postMessage(message); - } - - private __resolveWebview(webviewView: WebviewView) { - this.__webviewResolver.resolveWebview( - webviewView.webview, - "main", - JSON.stringify(this.__buildProps()), - "mainWebviewViewProps", - ); - } - - private __buildProps() { - return selectMainWebviewViewProps( - this.__store.getState(), - this.__rootUri, - this.__directoryPaths, - this.__executionQueue, - this.__codemodEngineNodeLocated, - ); - } - - private __onDidReceiveMessage = async (message: WebviewResponse) => { - if (message.kind === "webview.command") { - commands.executeCommand( - message.value.command, - ...(message.value.arguments ?? []), - ); - } - - if (message.kind === "webview.campaignManager.setSelectedCaseHash") { - this.__store.dispatch(actions.setSelectedCaseHash(message.caseHash)); - } - - if (message.kind === "webview.global.discardSelected") { - commands.executeCommand("codemod.discardJobs", message.caseHashDigest); - } - - if (message.kind === "webview.global.showInformationMessage") { - window.showInformationMessage(message.value); - } - - if (message.kind === "webview.global.applySelected") { - commands.executeCommand( - "codemod.sourceControl.saveStagedJobsToTheFileSystem", - message.caseHashDigest, - ); - } - - if (message.kind === "webview.main.setActiveTabId") { - this.__store.dispatch(actions.setActiveTabId(message.activeTabId)); - } - - if (message.kind === "webview.main.setCodemodDiscoveryPanelGroupSettings") { - this.__store.dispatch( - actions.setCodemodDiscoveryPanelGroupSettings( - message.panelGroupSettings, - ), - ); - } - - if (message.kind === "webview.main.setCodemodRunsPanelGroupSettings") { - this.__store.dispatch( - actions.setCodemodRunsPanelGroupSettings(message.panelGroupSettings), - ); - } - - if (message.kind === "webview.main.setToaster") { - this.__store.dispatch(actions.setToaster(message.value)); - } - - if (message.kind === "webview.global.flipSelectedExplorerNode") { - this.__store.dispatch( - actions.flipSelectedExplorerNode([ - message.caseHashDigest, - message.explorerNodeHashDigest, - ]), - ); - } - - if (message.kind === "webview.global.flipCollapsibleExplorerNode") { - this.__store.dispatch( - actions.flipCollapsibleExplorerNode([ - message.caseHashDigest, - message.explorerNodeHashDigest, - ]), - ); - } - - if (message.kind === "webview.global.focusExplorerNode") { - this.__store.dispatch( - actions.focusExplorerNode([ - message.caseHashDigest, - message.explorerNodeHashDigest, - ]), - ); - } - - if (message.kind === "webview.global.setChangeExplorerSearchPhrase") { - this.__store.dispatch( - actions.setChangeExplorerSearchPhrase([ - message.caseHashDigest, - message.searchPhrase, - ]), - ); - } - - if (message.kind === "webview.codemodList.haltCodemodExecution") { - this.__engineService.shutdownEngines(); - } - - if (message.kind === "webview.codemodList.dryRunCodemod") { - if (this.__rootUri === null) { - window.showWarningMessage("No active workspace is found."); - return; - } - - const hashDigest = message.value; - this.__store.dispatch(actions.setRecentCodemodHashes(hashDigest)); - - const state = this.__store.getState().codemodDiscoveryView; - const executionPath = - state.executionPaths[hashDigest] ?? this.__rootUri.fsPath; - - if (executionPath === null) { - return; - } - - const uri = Uri.file(executionPath); - - // if missing some required arguments, open arguments popup - - const argumentsSpecified = selectCodemodArguments( - this.__store.getState(), - hashDigest as unknown as CodemodNodeHashDigest, - ).every( - ({ required, value }) => - !required || (isNeitherNullNorUndefined(value) && value !== ""), - ); - - if (!argumentsSpecified) { - this.__store.dispatch( - actions.setCodemodArgumentsPopupHashDigest( - hashDigest as unknown as CodemodNodeHashDigest, - ), - ); - return; - } - - commands.executeCommand("codemod.executeCodemod", uri, hashDigest); - } - - if (message.kind === "webview.codemodList.updatePathToExecute") { - await this.updateExecutionPath(message.value); - - this.__postMessage({ - kind: "webview.main.setProps", - props: this.__buildProps(), - }); - } - - if (message.kind === "webview.global.showWarningMessage") { - window.showWarningMessage(message.value); - } - - if (message.kind === "webview.global.flipCodemodHashDigest") { - this.__store.dispatch( - actions.flipCodemodHashDigest(message.codemodNodeHashDigest), - ); - } - - if (message.kind === "webview.global.selectCodemodNodeHashDigest") { - this.__store.dispatch( - actions.setFocusedCodemodHashDigest( - message.selectedCodemodNodeHashDigest, - ), - ); - } - - if (message.kind === "webview.global.setCodemodSearchPhrase") { - this.__store.dispatch( - actions.setCodemodSearchPhrase(message.searchPhrase), - ); - } - - if (message.kind === "webview.global.collapseResultsPanel") { - this.__store.dispatch(actions.collapseResultsPanel(message.collapsed)); - } - - if (message.kind === "webview.global.collapseChangeExplorerPanel") { - this.__store.dispatch( - actions.collapseChangeExplorerPanel(message.collapsed), - ); - } - - if (message.kind === "webview.global.setCodemodArgumentsPopupHashDigest") { - this.__store.dispatch( - actions.setCodemodArgumentsPopupHashDigest(message.hashDigest), - ); - } - - if (message.kind === "webview.global.setCodemodArgument") { - this.__store.dispatch( - actions.setCodemodArgument({ - hashDigest: message.hashDigest, - name: message.name, - value: message.value, - }), - ); - } - }; - - public updateExecutionPath = async ({ - newPath, - codemodHash, - errorMessage, - warningMessage, - revertToPrevExecutionIfInvalid, - fromVSCodeCommand, - }: { - newPath: string; - codemodHash: CodemodHash; - errorMessage: string | null; - warningMessage: string | null; - revertToPrevExecutionIfInvalid: boolean; - fromVSCodeCommand?: boolean; - }) => { - if (this.__rootUri === null) { - window.showWarningMessage("No active workspace is found."); - return; - } - - const state = this.__store.getState().codemodDiscoveryView; - const persistedExecutionPath = state.executionPaths[codemodHash]; - - const oldExecutionPath = persistedExecutionPath ?? null; - const newPathAbsolute = relativeToAbsolutePath( - newPath, - this.__rootUri.fsPath, - ); - - try { - await workspace.fs.stat(Uri.file(newPathAbsolute)); - this.__store.dispatch( - actions.setExecutionPath({ - codemodHash, - path: newPathAbsolute, - }), - ); - - if (!fromVSCodeCommand) { - window.showInformationMessage( - "Successfully updated the execution path.", - ); - } - } catch (e) { - if (errorMessage !== null) { - window.showErrorMessage(errorMessage); - } - if (warningMessage !== null) { - window.showWarningMessage(warningMessage); - } - - if (oldExecutionPath === null) { - return; - } - - if (revertToPrevExecutionIfInvalid) { - this.__store.dispatch( - actions.setExecutionPath({ - codemodHash, - path: oldExecutionPath, - }), - ); - } else { - this.__store.dispatch( - actions.setExecutionPath({ - codemodHash, - path: oldExecutionPath, - }), - ); - } - } - }; + private __view: WebviewView | null = null; + private __webviewResolver: WebviewResolver; + private __executionQueue: ReadonlyArray<CodemodHash> = []; + private __directoryPaths: ReadonlyArray<string> | null = null; + // true by default to prevent banner blinking on load + private __codemodEngineNodeLocated = true; + + constructor( + context: ExtensionContext, + private readonly __engineService: EngineService, + private readonly __messageBus: MessageBus, + private readonly __rootUri: Uri | null, + private readonly __store: Store, + ) { + this.__webviewResolver = new WebviewResolver(context.extensionUri); + + this.__messageBus.subscribe(MessageKind.showProgress, (message) => { + if (message.codemodHash === null) { + return; + } + + this.__postMessage({ + kind: 'webview.global.setCodemodExecutionProgress', + codemodHash: message.codemodHash, + progressKind: message.progressKind, + totalFileNumber: message.totalFileNumber, + processedFileNumber: message.processedFileNumber, + }); + }); + + this.__messageBus.subscribe(MessageKind.codemodSetExecuted, () => { + this.__postMessage({ + kind: 'webview.global.codemodExecutionHalted', + }); + }); + + this.__messageBus.subscribe(MessageKind.executeCodemodSet, () => { + this.__store.dispatch(actions.collapseResultsPanel(false)); + this.__store.dispatch(actions.collapseChangeExplorerPanel(false)); + }); + + this.__messageBus.subscribe( + MessageKind.executionQueueChange, + (message) => { + this.__executionQueue = message.queuedCodemodHashes; + const props = this.__buildProps(); + + this.__postMessage({ + kind: 'webview.main.setProps', + props: props, + }); + }, + ); + + this.__messageBus.subscribe( + MessageKind.codemodEngineNodeLocated, + ({ codemodEngineNodeLocated }) => { + if ( + this.__codemodEngineNodeLocated === codemodEngineNodeLocated + ) { + return; + } + + this.__codemodEngineNodeLocated = codemodEngineNodeLocated; + + const props = this.__buildProps(); + + this.__postMessage({ + kind: 'webview.main.setProps', + props, + }); + }, + ); + + let prevProps = this.__buildProps(); + + this.__store.subscribe(async () => { + if (this.__directoryPaths === null) { + await this.__getDirectoryPaths(); + } + + const nextProps = this.__buildProps(); + if (areEqual(prevProps, nextProps)) { + return; + } + + prevProps = nextProps; + + this.__postMessage({ + kind: 'webview.main.setProps', + props: nextProps, + }); + }); + } + + public isVisible(): boolean { + return this.__view?.visible ?? false; + } + + public resolveWebviewView(webviewView: WebviewView): void | Thenable<void> { + this.__resolveWebview(webviewView); + + this.__view = webviewView; + + this.__view.webview.onDidReceiveMessage(this.__onDidReceiveMessage); + + this.__messageBus.publish({ + kind: MessageKind.mainWebviewViewVisibilityChange, + }); + + this.__view.onDidChangeVisibility(() => { + this.__messageBus.publish({ + kind: MessageKind.mainWebviewViewVisibilityChange, + }); + + if (this.__view?.visible) { + this.__resolveWebview(this.__view); + } + }); + } + + private async __getDirectoryPaths() { + if (this.__rootUri === null) { + return; + } + + const globPattern = buildGlobPattern(this.__rootUri, '/**'); + + // From `glob` documentation: + // (Note: to match only directories, put a / at the end of the pattern.) + const directoryPaths = await glob(`${globPattern}/`, { + // ignore node_modules and files, match only directories + ignore: ['**/node_modules/**'], + follow: false, + maxDepth: 10, + }); + + const MAX_NUMBER_OF_DIRECTORIES = 10000; + + this.__directoryPaths = directoryPaths.slice( + 0, + MAX_NUMBER_OF_DIRECTORIES, + ); + } + + private __postMessage(message: WebviewMessage) { + this.__view?.webview.postMessage(message); + } + + private __resolveWebview(webviewView: WebviewView) { + this.__webviewResolver.resolveWebview( + webviewView.webview, + 'main', + JSON.stringify(this.__buildProps()), + 'mainWebviewViewProps', + ); + } + + private __buildProps() { + return selectMainWebviewViewProps( + this.__store.getState(), + this.__rootUri, + this.__directoryPaths, + this.__executionQueue, + this.__codemodEngineNodeLocated, + ); + } + + private __onDidReceiveMessage = async (message: WebviewResponse) => { + if (message.kind === 'webview.command') { + commands.executeCommand( + message.value.command, + ...(message.value.arguments ?? []), + ); + } + + if (message.kind === 'webview.campaignManager.setSelectedCaseHash') { + this.__store.dispatch( + actions.setSelectedCaseHash(message.caseHash), + ); + } + + if (message.kind === 'webview.global.discardSelected') { + commands.executeCommand( + 'codemod.discardJobs', + message.caseHashDigest, + ); + } + + if (message.kind === 'webview.global.showInformationMessage') { + window.showInformationMessage(message.value); + } + + if (message.kind === 'webview.global.applySelected') { + commands.executeCommand( + 'codemod.sourceControl.saveStagedJobsToTheFileSystem', + message.caseHashDigest, + ); + } + + if (message.kind === 'webview.main.setActiveTabId') { + this.__store.dispatch(actions.setActiveTabId(message.activeTabId)); + } + + if ( + message.kind === + 'webview.main.setCodemodDiscoveryPanelGroupSettings' + ) { + this.__store.dispatch( + actions.setCodemodDiscoveryPanelGroupSettings( + message.panelGroupSettings, + ), + ); + } + + if (message.kind === 'webview.main.setCodemodRunsPanelGroupSettings') { + this.__store.dispatch( + actions.setCodemodRunsPanelGroupSettings( + message.panelGroupSettings, + ), + ); + } + + if (message.kind === 'webview.main.setToaster') { + this.__store.dispatch(actions.setToaster(message.value)); + } + + if (message.kind === 'webview.global.flipSelectedExplorerNode') { + this.__store.dispatch( + actions.flipSelectedExplorerNode([ + message.caseHashDigest, + message.explorerNodeHashDigest, + ]), + ); + } + + if (message.kind === 'webview.global.flipCollapsibleExplorerNode') { + this.__store.dispatch( + actions.flipCollapsibleExplorerNode([ + message.caseHashDigest, + message.explorerNodeHashDigest, + ]), + ); + } + + if (message.kind === 'webview.global.focusExplorerNode') { + this.__store.dispatch( + actions.focusExplorerNode([ + message.caseHashDigest, + message.explorerNodeHashDigest, + ]), + ); + } + + if (message.kind === 'webview.global.setChangeExplorerSearchPhrase') { + this.__store.dispatch( + actions.setChangeExplorerSearchPhrase([ + message.caseHashDigest, + message.searchPhrase, + ]), + ); + } + + if (message.kind === 'webview.codemodList.haltCodemodExecution') { + this.__engineService.shutdownEngines(); + } + + if (message.kind === 'webview.codemodList.dryRunCodemod') { + if (this.__rootUri === null) { + window.showWarningMessage('No active workspace is found.'); + return; + } + + const hashDigest = message.value; + this.__store.dispatch(actions.setRecentCodemodHashes(hashDigest)); + + const state = this.__store.getState().codemodDiscoveryView; + const executionPath = + state.executionPaths[hashDigest] ?? this.__rootUri.fsPath; + + if (executionPath === null) { + return; + } + + const uri = Uri.file(executionPath); + + // if missing some required arguments, open arguments popup + + const argumentsSpecified = selectCodemodArguments( + this.__store.getState(), + hashDigest as unknown as CodemodNodeHashDigest, + ).every( + ({ required, value }) => + !required || + (isNeitherNullNorUndefined(value) && value !== ''), + ); + + if (!argumentsSpecified) { + this.__store.dispatch( + actions.setCodemodArgumentsPopupHashDigest( + hashDigest as unknown as CodemodNodeHashDigest, + ), + ); + return; + } + + commands.executeCommand('codemod.executeCodemod', uri, hashDigest); + } + + if (message.kind === 'webview.codemodList.updatePathToExecute') { + await this.updateExecutionPath(message.value); + + this.__postMessage({ + kind: 'webview.main.setProps', + props: this.__buildProps(), + }); + } + + if (message.kind === 'webview.global.showWarningMessage') { + window.showWarningMessage(message.value); + } + + if (message.kind === 'webview.global.flipCodemodHashDigest') { + this.__store.dispatch( + actions.flipCodemodHashDigest(message.codemodNodeHashDigest), + ); + } + + if (message.kind === 'webview.global.selectCodemodNodeHashDigest') { + this.__store.dispatch( + actions.setFocusedCodemodHashDigest( + message.selectedCodemodNodeHashDigest, + ), + ); + } + + if (message.kind === 'webview.global.setCodemodSearchPhrase') { + this.__store.dispatch( + actions.setCodemodSearchPhrase(message.searchPhrase), + ); + } + + if (message.kind === 'webview.global.collapseResultsPanel') { + this.__store.dispatch( + actions.collapseResultsPanel(message.collapsed), + ); + } + + if (message.kind === 'webview.global.collapseChangeExplorerPanel') { + this.__store.dispatch( + actions.collapseChangeExplorerPanel(message.collapsed), + ); + } + + if ( + message.kind === 'webview.global.setCodemodArgumentsPopupHashDigest' + ) { + this.__store.dispatch( + actions.setCodemodArgumentsPopupHashDigest(message.hashDigest), + ); + } + + if (message.kind === 'webview.global.setCodemodArgument') { + this.__store.dispatch( + actions.setCodemodArgument({ + hashDigest: message.hashDigest, + name: message.name, + value: message.value, + }), + ); + } + }; + + public updateExecutionPath = async ({ + newPath, + codemodHash, + errorMessage, + warningMessage, + revertToPrevExecutionIfInvalid, + fromVSCodeCommand, + }: { + newPath: string; + codemodHash: CodemodHash; + errorMessage: string | null; + warningMessage: string | null; + revertToPrevExecutionIfInvalid: boolean; + fromVSCodeCommand?: boolean; + }) => { + if (this.__rootUri === null) { + window.showWarningMessage('No active workspace is found.'); + return; + } + + const state = this.__store.getState().codemodDiscoveryView; + const persistedExecutionPath = state.executionPaths[codemodHash]; + + const oldExecutionPath = persistedExecutionPath ?? null; + const newPathAbsolute = relativeToAbsolutePath( + newPath, + this.__rootUri.fsPath, + ); + + try { + await workspace.fs.stat(Uri.file(newPathAbsolute)); + this.__store.dispatch( + actions.setExecutionPath({ + codemodHash, + path: newPathAbsolute, + }), + ); + + if (!fromVSCodeCommand) { + window.showInformationMessage( + 'Successfully updated the execution path.', + ); + } + } catch (e) { + if (errorMessage !== null) { + window.showErrorMessage(errorMessage); + } + if (warningMessage !== null) { + window.showWarningMessage(warningMessage); + } + + if (oldExecutionPath === null) { + return; + } + + if (revertToPrevExecutionIfInvalid) { + this.__store.dispatch( + actions.setExecutionPath({ + codemodHash, + path: oldExecutionPath, + }), + ); + } else { + this.__store.dispatch( + actions.setExecutionPath({ + codemodHash, + path: oldExecutionPath, + }), + ); + } + } + }; } diff --git a/packages/codemods/axios/fetch/__testfixtures__/fixture1.input.ts b/packages/codemods/axios/fetch/__testfixtures__/fixture1.input.ts index e6ef09e31..c0ef47581 100644 --- a/packages/codemods/axios/fetch/__testfixtures__/fixture1.input.ts +++ b/packages/codemods/axios/fetch/__testfixtures__/fixture1.input.ts @@ -1,3 +1,3 @@ -const { data } = await axios.get(url, { - responseType: "arraybuffer", -}); \ No newline at end of file +const response = await fetch(url, { signal: AbortSignal.timeout(5000) }); +if (!response.ok) throw new Error('Network response was not ok'); +const data = await response.arrayBuffer(); diff --git a/packages/codemods/axios/fetch/__testfixtures__/fixture2.input.ts b/packages/codemods/axios/fetch/__testfixtures__/fixture2.input.ts index 0da333077..7b6280635 100644 --- a/packages/codemods/axios/fetch/__testfixtures__/fixture2.input.ts +++ b/packages/codemods/axios/fetch/__testfixtures__/fixture2.input.ts @@ -1,15 +1,20 @@ -const { data } = await axios.post( - `https://app.posthog.com/api/projects/${this.__projectId}/query/`, - { - query: { - kind: "HogQLQuery", - query: - "select properties.codemodName, count(*) from events where event in ('codemod.CLI.codemodExecuted', 'codemod.VSCE.codemodExecuted') group by properties.codemodName", - }, - }, - { - headers: { - Authorization: this.__authHeader, - }, - }, -); \ No newline at end of file +const response = await fetch( + `https://app.posthog.com/api/projects/${this.__projectId}/query/`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: this.__authHeader, + }, + body: JSON.stringify({ + query: { + kind: 'HogQLQuery', + query: "select properties.codemodName, count(*) from events where event in ('codemod.CLI.codemodExecuted', 'codemod.VSCE.codemodExecuted') group by properties.codemodName", + }, + }), + }, +); +if (!response.ok) { + throw new Error('Network response was not ok'); +} +const { data } = await response.json(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06193d25c..334c8a7f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3584,6 +3584,30 @@ importers: specifier: ^1.0.1 version: 1.5.1(@types/node@20.12.7)(jsdom@23.2.0)(terser@5.30.4) + packages/codemods/nuxt/4/default-data-error-value: + devDependencies: + '@codemod-com/utilities': + specifier: workspace:* + version: link:../../../../utilities + '@types/jscodeshift': + specifier: ^0.11.10 + version: 0.11.11 + '@vitest/coverage-v8': + specifier: ^1.0.1 + version: 1.5.1(vitest@1.5.1(@types/node@20.12.7)(jsdom@23.2.0)(terser@5.30.4)) + jscodeshift: + specifier: ^0.15.1 + version: 0.15.2(@babel/preset-env@7.24.4) + ts-node: + specifier: ^10.9.1 + version: 10.9.2(@types/node@20.12.7)(typescript@5.4.5) + typescript: + specifier: ^5.2.2 + version: 5.4.5 + vitest: + specifier: ^1.0.1 + version: 1.5.1(@types/node@20.12.7)(jsdom@23.2.0)(terser@5.30.4) + packages/codemods/nuxt/4/shallow-data-reactivity: devDependencies: '@codemod-com/utilities': @@ -4935,8 +4959,6 @@ importers: specifier: ^5.4.5 version: 5.4.5 - packages/database/generated/client: {} - packages/filemod: devDependencies: '@types/node': @@ -25920,7 +25942,7 @@ snapshots: eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.56.0) eslint-plugin-react: 7.34.1(eslint@8.56.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.56.0) @@ -25948,7 +25970,7 @@ snapshots: enhanced-resolve: 5.16.0 eslint: 8.56.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) fast-glob: 3.3.2 get-tsconfig: 4.7.3 is-core-module: 2.13.1 @@ -25970,7 +25992,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 From 81ee30e3a423955fa71af0aa75c1368424a511c5 Mon Sep 17 00:00:00 2001 From: Aleksy Rybicki <alekso.php@gmail.com> Date: Thu, 27 Jun 2024 11:42:26 +0100 Subject: [PATCH 2/4] Manual changes for axios migration --- apps/auth-service/package.json | 1 - apps/backend/package.json | 1 - apps/backend/src/plugins/authPlugin.ts | 230 ++-- apps/backend/src/publishHandler.test.ts | 40 +- apps/backend/src/publishHandler.ts | 31 +- apps/backend/src/services/GithubProvider.ts | 326 +++-- apps/backend/src/services/PostHogService.ts | 94 +- apps/backend/src/services/SourceControl.ts | 34 +- apps/cli/package.json | 5 +- apps/cli/src/apis.ts | 143 ++- apps/cli/src/buildCodemodOptions.ts | 6 +- apps/cli/src/commands/publish.ts | 7 +- apps/cli/src/commands/run.ts | 6 +- apps/cli/src/commands/unpublish.ts | 6 +- apps/cli/src/downloadCodemod.ts | 10 +- apps/cli/src/executeMainThread.ts | 23 +- apps/cli/src/fileDownloadService.ts | 21 +- .../(website)/studio/src/api/getCodeDiff.ts | 10 +- .../studio/src/api/populateLoginIntent.ts | 11 +- .../(website)/studio/src/api/sendMessage.ts | 31 +- .../app/(website)/studio/src/hooks/useAPI.ts | 41 +- apps/frontend/package.json | 1 - apps/frontend/utils/apis/client.ts | 53 +- apps/modgpt/package.json | 1 - apps/modgpt/src/plugins/authPlugin.ts | 17 +- apps/shared/mocks/gh-run.ts | 20 +- apps/task-manager/package.json | 4 +- apps/task-manager/src/services/Auth.ts | 77 +- .../src/services/GithubProvider.ts | 25 +- apps/task-manager/src/util.ts | 64 +- apps/vsce/package.json | 4 +- apps/vsce/src/axios/index.ts | 8 - apps/vsce/src/components/downloadService.ts | 29 +- apps/vsce/src/components/engineService.ts | 13 +- .../src/components/webview/MainProvider.ts | 1059 ++++++++--------- apps/vsce/src/fetch/index.ts | 23 + apps/vsce/test/dowloadService.test.ts | 31 +- apps/vsce/tsconfig.json | 2 +- packages/codemods/axios/fetch/src/index.ts | 4 + packages/runner/package.json | 4 +- packages/utilities/src/fetch.ts | 45 + packages/utilities/src/index.ts | 1 + packages/utilities/tsconfig.json | 1 + packages/workflow/package.json | 4 +- pnpm-lock.yaml | 243 +++- 45 files changed, 1483 insertions(+), 1327 deletions(-) delete mode 100644 apps/vsce/src/axios/index.ts create mode 100644 apps/vsce/src/fetch/index.ts create mode 100644 packages/utilities/src/fetch.ts diff --git a/apps/auth-service/package.json b/apps/auth-service/package.json index ebea35b33..a581c0eb7 100644 --- a/apps/auth-service/package.json +++ b/apps/auth-service/package.json @@ -28,7 +28,6 @@ "@fastify/busboy": "^2.1.1", "@fastify/cors": "8.5.0", "@fastify/rate-limit": "9.0.1", - "axios": "^1.6.8", "dotenv": "^16.4.5", "fastify": "4.25.1", "valibot": "^0.24.1" diff --git a/apps/backend/package.json b/apps/backend/package.json index fc78549e7..867de75ce 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -43,7 +43,6 @@ "@fastify/rate-limit": "9.0.1", "@types/tar": "^6.1.11", "ai": "2.2.29", - "axios": "^1.6.8", "bullmq": "^5.7.5", "chatgpt": "5.2.5", "chromadb": "1.7.2", diff --git a/apps/backend/src/plugins/authPlugin.ts b/apps/backend/src/plugins/authPlugin.ts index 9d0fe3a83..8b3201d37 100644 --- a/apps/backend/src/plugins/authPlugin.ts +++ b/apps/backend/src/plugins/authPlugin.ts @@ -1,124 +1,132 @@ -import type { OrganizationMembership, User } from '@codemod-com/utilities'; -import axios from 'axios'; -import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; -import fp from 'fastify-plugin'; -import { environment } from '../util'; +import { + type OrganizationMembership, + type User, + extendedFetch, +} from "@codemod-com/utilities"; +import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import fp from "fastify-plugin"; +import { environment } from "../util"; export interface UserDataPopulatedRequest extends FastifyRequest { - user?: User; - organizations?: OrganizationMembership[]; - allowedNamespaces?: string[]; + user?: User; + organizations?: OrganizationMembership[]; + allowedNamespaces?: string[]; } export interface OAuthTokenPopulatedRequest extends FastifyRequest { - token?: string; + token?: string; } -declare module 'fastify' { - interface FastifyInstance { - authenticate: ( - request: FastifyRequest, - reply: FastifyReply, - ) => Promise<void>; - getUserData: ( - request: FastifyRequest & UserDataPopulatedRequest, - reply: FastifyReply, - ) => Promise<void>; - getOAuthToken: ( - request: FastifyRequest & OAuthTokenPopulatedRequest, - reply: FastifyReply, - ) => Promise<void>; - } +declare module "fastify" { + interface FastifyInstance { + authenticate: ( + request: FastifyRequest, + reply: FastifyReply, + ) => Promise<void>; + getUserData: ( + request: FastifyRequest & UserDataPopulatedRequest, + reply: FastifyReply, + ) => Promise<void>; + getOAuthToken: ( + request: FastifyRequest & OAuthTokenPopulatedRequest, + reply: FastifyReply, + ) => Promise<void>; + } } async function authPlugin(fastify: FastifyInstance, _opts: unknown) { - fastify.decorate( - 'authenticate', - async (request: FastifyRequest, reply: FastifyReply) => { - try { - const authHeader = request.headers.authorization; - - if (!authHeader) - reply.code(401).send({ error: 'Unauthorized' }); - - await fetch(`${environment.AUTH_SERVICE_URL}/verifyToken`, { - headers: { Authorization: authHeader }, - }); - } catch (error) { - console.error(error); - reply.code(401).send({ error: 'Unauthorized' }); - } - }, - ); - - fastify.decorate( - 'getUserData', - async ( - request: FastifyRequest & { - user?: User; - organizations?: OrganizationMembership[]; - allowedNamespaces?: string[]; - }, - reply: FastifyReply, - ) => { - try { - const authHeader = request.headers.authorization; - - if (!authHeader) { - request.user = undefined; - request.organizations = undefined; - request.allowedNamespaces = undefined; - return; - } - - const response = await fetch( - `${environment.AUTH_SERVICE_URL}/userData`, - { headers: { Authorization: authHeader } }, - ); - if (!response.ok) throw new Error('Failed to fetch userData'); - const { data } = { data: await response.json() }; - - const { user, organizations, allowedNamespaces } = data; - - request.user = user; - request.organizations = organizations; - request.allowedNamespaces = allowedNamespaces; - } catch (error) { - console.error(error); - reply.code(401).send({ error: 'Unauthorized' }); - } - }, - ); - - fastify.decorate( - 'getOAuthToken', - async ( - request: FastifyRequest & { - token?: string; - }, - reply: FastifyReply, - ) => { - try { - const authHeader = request.headers.authorization; - - if (!authHeader) - reply.code(401).send({ error: 'Unauthorized' }); - - const response = await fetch( - `${environment.AUTH_SERVICE_URL}/oAuthToken`, - { headers: { Authorization: authHeader } }, - ); - if (!response.ok) throw new Error('Failed to fetch oAuthToken'); - const { data } = { data: await response.json() }; - - const { token } = data; - - request.token = token; - } catch { - reply.code(401).send({ error: 'Unauthorized' }); - } - }, - ); + fastify.decorate( + "authenticate", + async (request: FastifyRequest, reply: FastifyReply) => { + try { + const authHeader = request.headers.authorization; + + if (!authHeader) { + reply.code(401).send({ error: "Unauthorized" }); + return; + } + + await extendedFetch(`${environment.AUTH_SERVICE_URL}/verifyToken`, { + headers: { Authorization: authHeader }, + }); + } catch (error) { + console.error(error); + reply.code(401).send({ error: "Unauthorized" }); + } + }, + ); + + fastify.decorate( + "getUserData", + async ( + request: FastifyRequest & { + user?: User; + organizations?: OrganizationMembership[]; + allowedNamespaces?: string[]; + }, + reply: FastifyReply, + ) => { + try { + const authHeader = request.headers.authorization; + + if (!authHeader) { + request.user = undefined; + request.organizations = undefined; + request.allowedNamespaces = undefined; + return; + } + + const response = await extendedFetch( + `${environment.AUTH_SERVICE_URL}/userData`, + { headers: { Authorization: authHeader } }, + ); + + const { user, organizations, allowedNamespaces } = + (await response.json()) as { + user?: User; + organizations?: OrganizationMembership[]; + allowedNamespaces?: string[]; + }; + + request.user = user; + request.organizations = organizations; + request.allowedNamespaces = allowedNamespaces; + } catch (error) { + console.error(error); + reply.code(401).send({ error: "Unauthorized" }); + } + }, + ); + + fastify.decorate( + "getOAuthToken", + async ( + request: FastifyRequest & { + token?: string; + }, + reply: FastifyReply, + ) => { + try { + const authHeader = request.headers.authorization; + + if (!authHeader) { + reply.code(401).send({ error: "Unauthorized" }); + return; + } + + const response = await extendedFetch( + `${environment.AUTH_SERVICE_URL}/oAuthToken`, + { headers: { Authorization: authHeader } }, + ); + + const { token } = (await response.json()) as { token?: string }; + + request.token = token; + } catch { + reply.code(401).send({ error: "Unauthorized" }); + } + }, + ); } export default fp(authPlugin); diff --git a/apps/backend/src/publishHandler.test.ts b/apps/backend/src/publishHandler.test.ts index 054f02807..1dbff7730 100644 --- a/apps/backend/src/publishHandler.test.ts +++ b/apps/backend/src/publishHandler.test.ts @@ -20,6 +20,8 @@ const GET_USER_RETURN = { const MOCK_TIMESTAMP = "timestamp"; +const originalFetch = global.fetch; + const mocks = vi.hoisted(() => { const S3Client = vi.fn(); S3Client.prototype.send = vi.fn(); @@ -44,12 +46,16 @@ const mocks = vi.hoisted(() => { findUnique: vi.fn(), }, }, - axios: { - get: vi.fn().mockImplementation((url: string, ...args: unknown[]) => ({ - data: GET_USER_RETURN, - })), - post: vi.fn().mockImplementation(() => ({})), - }, + fetch: vi.fn().mockImplementation((url, options) => { + if (options.method === "GET") { + return Promise.resolve({ + json: () => Promise.resolve(GET_USER_RETURN), + ok: true, + }); + } + + return Promise.resolve({ json: () => GET_USER_RETURN, ok: true }); + }), S3Client, TarService, PutObjectCommand, @@ -62,10 +68,6 @@ vi.mock("@codemod-com/database", async () => { return { ...actual, prisma: mocks.prisma }; }); -vi.mock("axios", async () => { - return { default: mocks.axios }; -}); - vi.mock("@aws-sdk/client-s3", async () => { const actual = await vi.importActual("@aws-sdk/client-s3"); @@ -116,7 +118,7 @@ vi.mock("@codemod-com/utilities", async () => { }; }); -vi.stubGlobal("fetch", vi.fn()); +vi.stubGlobal("fetch", mocks.fetch); describe("/publish route", async () => { const fastify = await runServer(); @@ -218,8 +220,8 @@ describe("/publish route", async () => { requestTimeout: 5000, }); - expect(mocks.axios.post).toHaveBeenCalledOnce(); - expect(mocks.axios.post).toHaveBeenCalledWith( + expect(mocks.fetch).toHaveBeenCalledOnce(); + expect(mocks.fetch).toHaveBeenCalledWith( "https://hooks.zapier.com/hooks/catch/18983913/2ybuovt/", { codemod: { @@ -605,8 +607,8 @@ describe("/publish route", async () => { describe("when publishing via org", async () => { it("should go through happy path if user has access to the org", async () => { mocks.prisma.codemodVersion.findFirst.mockImplementation(() => null); - mocks.axios.get.mockImplementation(() => ({ - data: { ...GET_USER_RETURN, allowedNamespaces: ["org"] }, + mocks.fetch.mockImplementation(() => ({ + json: () => ({ ...GET_USER_RETURN, allowedNamespaces: ["org"] }), })); mocks.prisma.codemod.upsert.mockImplementation(() => { return { createdAt: { getTime: () => MOCK_TIMESTAMP }, id: "id" }; @@ -669,8 +671,12 @@ describe("/publish route", async () => { it("should fail if user has no access to the org", async () => { mocks.prisma.codemodVersion.findFirst.mockImplementation(() => null); - mocks.axios.get.mockImplementation(() => ({ - data: { ...GET_USER_RETURN, organizations: [], allowedNamespaces: [] }, + mocks.fetch.mockImplementation(() => ({ + json: () => ({ + ...GET_USER_RETURN, + organizations: [], + allowedNamespaces: [], + }), })); const codemodRcContents: CodemodConfigInput = { diff --git a/apps/backend/src/publishHandler.ts b/apps/backend/src/publishHandler.ts index 1982b81c5..ac7b3224d 100644 --- a/apps/backend/src/publishHandler.ts +++ b/apps/backend/src/publishHandler.ts @@ -7,10 +7,10 @@ import { TarService, buildCodemodSlug, codemodNameRegex, + extendedFetch, isNeitherNullNorUndefined, parseCodemodConfig, } from "@codemod-com/utilities"; -import axios from "axios"; import type { RouteHandler } from "fastify"; import * as semver from "semver"; import type { UserDataPopulatedRequest } from "./plugins/authPlugin"; @@ -350,7 +350,34 @@ export const publishHandler: RouteHandler<{ if (latestVersion === null) { try { - await const controller = new AbortController();const signal = controller.signal;setTimeout(() => controller.abort(), 5000);try { const response = await fetch( "https://hooks.zapier.com/hooks/catch/18983913/2ybuovt/", { method: 'POST', body: JSON.stringify({ codemod: { name, from: codemodRc.applicability?.from?.map((tuple) => tuple.join(" ")), to: codemodRc.applicability?.to?.map((tuple) => tuple.join(" ")), engine: codemodRc.engine, publishedAt: createdAtTimestamp, }, author: { username, name: `${firstName ?? ""} ${lastName ?? ""}`.trim() || null, email: emailAddresses.find((e) => e.id === primaryEmailAddressId)?.emailAddress ?? null, }, }), headers: { 'Content-Type': 'application/json' }, signal: signal } ); if (!response.ok) { throw new Error('Network response was not ok'); } const result = { data: await response.json() };} catch (err) { console.error("Failed calling Zapier hook:", err);}; + await extendedFetch( + "https://hooks.zapier.com/hooks/catch/18983913/2ybuovt/", + { + method: "POST", + body: JSON.stringify({ + codemod: { + name, + from: codemodRc.applicability?.from?.map((tuple) => + tuple.join(" "), + ), + to: codemodRc.applicability?.to?.map((tuple) => + tuple.join(" "), + ), + engine: codemodRc.engine, + publishedAt: createdAtTimestamp, + }, + author: { + username, + name: `${firstName ?? ""} ${lastName ?? ""}`.trim() || null, + email: + emailAddresses.find((e) => e.id === primaryEmailAddressId) + ?.emailAddress ?? null, + }, + }), + headers: { "Content-Type": "application/json" }, + signal: AbortSignal.timeout(5000), + }, + ); } catch (err) { console.error("Failed calling Zapier hook:", err); } diff --git a/apps/backend/src/services/GithubProvider.ts b/apps/backend/src/services/GithubProvider.ts index c886c389c..b33e0ed7d 100644 --- a/apps/backend/src/services/GithubProvider.ts +++ b/apps/backend/src/services/GithubProvider.ts @@ -1,195 +1,169 @@ -import axios, { type AxiosResponse } from 'axios'; -import gh from 'parse-github-url'; +import { extendedFetch } from "@codemod-com/utilities"; +import gh from "parse-github-url"; import type { - Assignee, - CreatePRParams, - GHBranch, - GithubContent, - GithubRepository, - Issue, - ListPRParams, - NewIssueParams, - PullRequest, - SourceControlProvider, -} from './SourceControl.js'; + Assignee, + CreatePRParams, + GithubContent, + GithubRepository, + Issue, + ListPRParams, + NewIssueParams, + PullRequest, + SourceControlProvider, +} from "./SourceControl.js"; type Repository = { - owner: string; - name: string; + owner: string; + name: string; }; class InvalidGithubUrlError extends Error {} class ParseGithubUrlError extends Error {} function parseGithubRepoUrl(url: string): Repository { - try { - const { owner, name } = gh(url) ?? {}; - - if (!owner || !name) { - throw new InvalidGithubUrlError('Missing owner or name'); - } - - return { owner, name }; - } catch (e) { - if (e instanceof InvalidGithubUrlError) { - throw e; - } - - const errorMessage = e instanceof Error ? e.message : String(e); - throw new ParseGithubUrlError(errorMessage); - } + try { + const { owner, name } = gh(url) ?? {}; + + if (!owner || !name) { + throw new InvalidGithubUrlError("Missing owner or name"); + } + + return { owner, name }; + } catch (e) { + if (e instanceof InvalidGithubUrlError) { + throw e; + } + + const errorMessage = e instanceof Error ? e.message : String(e); + throw new ParseGithubUrlError(errorMessage); + } } const withPagination = async ( - paginatedRequest: (page: string) => Promise<AxiosResponse<any[]>>, + paginatedRequest: (page: string) => Promise<Response>, ) => { - const nextPattern = /(?<=<)([\S]*)(?=>; rel="Next")/i; - let nextPage: string | null = '1'; - let data: any[] = []; - - while (nextPage !== null) { - const response = await paginatedRequest(nextPage); - data = [...data, ...(response.data ?? [])]; - - const linkHeader = response.headers.link; - - if ( - typeof linkHeader === 'string' && - linkHeader.includes(`rel=\"next\"`) - ) { - const nextUrl = linkHeader.match(nextPattern)?.[0]; - nextPage = nextUrl - ? new URL(nextUrl).searchParams.get('page') - : null; - } else { - nextPage = null; - } - } - - return data; + const nextPattern = /(?<=<)([\S]*)(?=>; rel="Next")/i; + let nextPage: string | null = "1"; + let data: any[] = []; + + while (nextPage !== null) { + const response = await paginatedRequest(nextPage); + data = [...data, ...(((await response.json()) as any[]) ?? [])]; + + const linkHeader = response.headers.get("link"); + + if (typeof linkHeader === "string" && linkHeader.includes(`rel=\"next\"`)) { + const nextUrl = linkHeader.match(nextPattern)?.[0]; + nextPage = nextUrl ? new URL(nextUrl).searchParams.get("page") : null; + } else { + nextPage = null; + } + } + + return data; }; const PER_PAGE = 99; export class GithubProvider implements SourceControlProvider { - private readonly __repo: string | null = null; - private readonly __baseUrl: string; - private readonly __authHeader: string; - - constructor(oAuthToken: string, repoUrl: string | null) { - this.__baseUrl = 'https://api.github.com'; - this.__repo = repoUrl; - this.__authHeader = `Bearer ${oAuthToken}`; - } - - private get __repoUrl() { - const { owner, name } = parseGithubRepoUrl(this.__repo ?? ''); - - return `${this.__baseUrl}/repos/${owner}/${name}`; - } - - async createIssue(params: NewIssueParams): Promise<Issue> { - const response = await fetch(`${this.__repoUrl}/issues`, { - method: 'POST', - headers: { - Authorization: this.__authHeader, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(params), - }); - if (!response.ok) throw new Error('Network response was not ok'); - const res = { data: (await response.json()) as Issue }; - - return res.data; - } - - async createPullRequest(params: CreatePRParams): Promise<PullRequest> { - const response = await fetch(`${this.__repoUrl}/pulls`, { - method: 'POST', - headers: { - Authorization: this.__authHeader, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(params), - }); - if (!response.ok) throw new Error('Network response was not ok'); - const res = { data: (await response.json()) as PullRequest }; - - return res.data; - } - - async getPullRequests(params: ListPRParams): Promise<PullRequest[]> { - const queryParams = Object.entries(params).reduce< - Record<string, string> - >((acc, [key, value]) => { - if (value) { - acc[key] = value; - } - - return acc; - }, {}); - - const query = new URLSearchParams(queryParams).toString(); - - const response = await fetch(`${this.__repoUrl}/pulls?${query}`, { - headers: { Authorization: this.__authHeader }, - }); - if (!response.ok) throw new Error('Network response was not ok'); - const res = { data: (await response.json()) as PullRequest[] }; - - return res.data; - } - - async getAssignees(): Promise<Assignee[]> { - const response = await fetch(`${this.__repoUrl}/assignees`, { - headers: { Authorization: this.__authHeader }, - }); - if (!response.ok) throw new Error('Network response was not ok'); - const res = { data: (await response.json()) as Assignee[] }; - - return res.data; - } - - private __getUserRepositories = async ( - page: string, - ): Promise<AxiosResponse<GithubRepository[]>> => { - return await axios.get<GithubRepository[]>( - `https://api.github.com/user/repos?per_page=${PER_PAGE}&page=${page}`, - { - headers: { - Authorization: this.__authHeader, - }, - }, - ); - }; - - async getUserRepositories(): Promise<GithubRepository[]> { - return await withPagination(this.__getUserRepositories); - } - - private __getBranches = async ( - page: string, - ): Promise<AxiosResponse<GHBranch[]>> => { - const response = await fetch( - `${this.__repoUrl}/branches?per_page=${PER_PAGE}&page=${page}`, - { headers: { Authorization: this.__authHeader } }, - ); - if (!response.ok) throw new Error('Network response was not ok'); - return { data: (await response.json()) as GHBranch[] }; - }; - - async getBranches(): Promise<string[]> { - return await withPagination(this.__getBranches); - } - - async getRepoContents(branchName: string): Promise<GithubContent[]> { - const response = await fetch( - `${this.__repoUrl}/contents?ref=${branchName}`, - { headers: { Authorization: this.__authHeader } }, - ); - if (!response.ok) throw new Error('Network response was not ok'); - const res = { data: (await response.json()) as GithubContent[] }; - - return res.data; - } + private readonly __repo: string | null = null; + private readonly __baseUrl: string; + private readonly __authHeader: string; + + constructor(oAuthToken: string, repoUrl: string | null) { + this.__baseUrl = "https://api.github.com"; + this.__repo = repoUrl; + this.__authHeader = `Bearer ${oAuthToken}`; + } + + private get __repoUrl() { + const { owner, name } = parseGithubRepoUrl(this.__repo ?? ""); + + return `${this.__baseUrl}/repos/${owner}/${name}`; + } + + async createIssue(params: NewIssueParams) { + const response = await extendedFetch(`${this.__repoUrl}/issues`, { + method: "POST", + headers: { + Authorization: this.__authHeader, + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + + return (await response.json()) as Issue; + } + + async createPullRequest(params: CreatePRParams) { + const response = await extendedFetch(`${this.__repoUrl}/pulls`, { + method: "POST", + headers: { + Authorization: this.__authHeader, + "Content-Type": "application/json", + }, + body: JSON.stringify(params), + }); + + return (await response.json()) as PullRequest; + } + + async getPullRequests(params: ListPRParams) { + const queryParams = Object.entries(params).reduce<Record<string, string>>( + (acc, [key, value]) => { + if (value) { + acc[key] = value; + } + + return acc; + }, + {}, + ); + + const query = new URLSearchParams(queryParams).toString(); + + const response = await extendedFetch(`${this.__repoUrl}/pulls?${query}`, { + headers: { Authorization: this.__authHeader }, + }); + + return (await response.json()) as PullRequest[]; + } + + async getAssignees() { + const response = await extendedFetch(`${this.__repoUrl}/assignees`, { + headers: { Authorization: this.__authHeader }, + }); + + return (await response.json()) as Assignee[]; + } + + private __getUserRepositories = (page: string) => + extendedFetch( + `https://api.github.com/user/repos?per_page=${PER_PAGE}&page=${page}`, + { headers: { Authorization: this.__authHeader } }, + ); + + async getUserRepositories(): Promise<GithubRepository[]> { + return await withPagination(this.__getUserRepositories); + } + + private __getBranches = (page: string) => + extendedFetch( + `${this.__repoUrl}/branches?per_page=${PER_PAGE}&page=${page}`, + { + headers: { Authorization: this.__authHeader }, + }, + ); + + async getBranches(): Promise<string[]> { + return await withPagination(this.__getBranches); + } + + async getRepoContents(branchName: string) { + const response = await extendedFetch( + `${this.__repoUrl}/contents?ref=${branchName}`, + { headers: { Authorization: this.__authHeader } }, + ); + return (await response.json()) as GithubContent[]; + } } diff --git a/apps/backend/src/services/PostHogService.ts b/apps/backend/src/services/PostHogService.ts index ff3c06d62..4c4f609fa 100644 --- a/apps/backend/src/services/PostHogService.ts +++ b/apps/backend/src/services/PostHogService.ts @@ -1,59 +1,55 @@ -import { buildCodemodSlug } from '@codemod-com/utilities'; -import axios, { isAxiosError } from 'axios'; +import { + buildCodemodSlug, + extendedFetch, + isFetchError, +} from "@codemod-com/utilities"; export class PostHogCodemodNotFoundError extends Error {} export class PostHogService { - private readonly __authHeader: string; - private readonly __projectId: string; + private readonly __authHeader: string; + private readonly __projectId: string; - constructor(authKey: string, projectId: string) { - this.__authHeader = `Bearer ${authKey}`; - this.__projectId = projectId; - } + constructor(authKey: string, projectId: string) { + this.__authHeader = `Bearer ${authKey}`; + this.__projectId = projectId; + } - async getCodemodTotalRuns(): Promise< - Array<{ slug: string; runs: number }> - > { - try { - const response = await fetch( - `https://app.posthog.com/api/projects/${this.__projectId}/query/`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: this.__authHeader, - }, - body: JSON.stringify({ - query: { - kind: 'HogQLQuery', - query: "select properties.codemodName, count(*) from events where event in ('codemod.CLI.codemodExecuted', 'codemod.VSCE.codemodExecuted') group by properties.codemodName limit 500", - }, - }), - }, - ); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const data = await response.json(); + async getCodemodTotalRuns(): Promise<Array<{ slug: string; runs: number }>> { + try { + const response = await extendedFetch( + `https://app.posthog.com/api/projects/${this.__projectId}/query/`, + { + method: "POST", + headers: { Authorization: this.__authHeader }, + body: JSON.stringify({ + query: { + kind: "HogQLQuery", + query: + "select properties.codemodName, count(*) from events where event in ('codemod.CLI.codemodExecuted', 'codemod.VSCE.codemodExecuted') group by properties.codemodName limit 500", + }, + }), + }, + ); + const { data } = (await response.json()) as { + data: { results: Array<[string, number]> }; + }; - const result = data?.results?.map((value: [string, number]) => ({ - // @TODO add isLocal field to telemetry event, exclude local events from total runs - slug: buildCodemodSlug( - value[0].replaceAll(' (from user machine)', ''), - ), - runs: value[1], - })); + const result = data?.results?.map((value: [string, number]) => ({ + // @TODO add isLocal field to telemetry event, exclude local events from total runs + slug: buildCodemodSlug(value[0].replaceAll(" (from user machine)", "")), + runs: value[1], + })); - return result; - } catch (error) { - const errorMessage = isAxiosError<{ message: string }>(error) - ? error.response?.data.message - : (error as Error).message; + return result; + } catch (error) { + const errorMessage = isFetchError(error) + ? ((await error.response?.json()) as { message: string }).message + : (error as Error).message; - throw new PostHogCodemodNotFoundError( - `Failed to retrieve events. Reason: ${errorMessage}`, - ); - } - } + throw new PostHogCodemodNotFoundError( + `Failed to retrieve events. Reason: ${errorMessage}`, + ); + } + } } diff --git a/apps/backend/src/services/SourceControl.ts b/apps/backend/src/services/SourceControl.ts index ce65f87a7..2c5c504eb 100644 --- a/apps/backend/src/services/SourceControl.ts +++ b/apps/backend/src/services/SourceControl.ts @@ -1,4 +1,4 @@ -import { isAxiosError } from "axios"; +import { isFetchError } from "@codemod-com/utilities"; export type NewIssueParams = Readonly<{ body: string; @@ -93,13 +93,17 @@ export interface SourceControlProvider { // biome-ignore lint/complexity/noStaticOnlyClass: reason? export class SourceControlError extends Error { - static parse(e: unknown) { - const message = - isAxiosError(e) && e.response?.data.message - ? e.response?.data.message - : e instanceof Error - ? e.message - : String(e); + static async parse(e: unknown) { + let message: any; + if (isFetchError(e)) { + const response = (await e.response?.json()) as { message?: string }; + if (response.message) { + message = response.message; + } + } + if (!message) { + message = e instanceof Error ? e.message : String(e); + } return new SourceControlError(message); } } @@ -112,7 +116,7 @@ export class SourceControl { try { return await provider.createIssue(params); } catch (e) { - throw SourceControlError.parse(e); + throw await SourceControlError.parse(e); } } @@ -123,7 +127,7 @@ export class SourceControl { try { return await provider.createPullRequest(params); } catch (e) { - throw SourceControlError.parse(e); + throw await SourceControlError.parse(e); } } @@ -134,7 +138,7 @@ export class SourceControl { try { return await provider.getPullRequests(params); } catch (e) { - throw SourceControlError.parse(e); + throw await SourceControlError.parse(e); } } @@ -142,7 +146,7 @@ export class SourceControl { try { return await provider.getAssignees(); } catch (e) { - throw SourceControlError.parse(e); + throw await SourceControlError.parse(e); } } @@ -152,7 +156,7 @@ export class SourceControl { try { return await provider.getUserRepositories(); } catch (e) { - throw SourceControlError.parse(e); + throw await SourceControlError.parse(e); } } @@ -160,7 +164,7 @@ export class SourceControl { try { return await provider.getBranches(); } catch (e) { - throw SourceControlError.parse(e); + throw await SourceControlError.parse(e); } } @@ -171,7 +175,7 @@ export class SourceControl { try { return await provider.getRepoContents(branchName); } catch (e) { - throw SourceControlError.parse(e); + throw await SourceControlError.parse(e); } } } diff --git a/apps/cli/package.json b/apps/cli/package.json index 15deea8b0..676b7ea6b 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -43,7 +43,6 @@ "@types/unzipper": "^0.10.9", "@types/yargs": "^17.0.13", "@vitest/coverage-v8": "^1.0.1", - "axios": "^1.6.8", "columnify": "^1.6.0", "cosmiconfig": "^8.3.6", "exponential-backoff": "^3.1.1", @@ -82,8 +81,8 @@ "access": "public" }, "dependencies": { - "@ast-grep/cli": "^0.22.3", - "@ast-grep/napi": "^0.22.3", + "@ast-grep/cli": "^0.24.0", + "@ast-grep/napi": "^0.24.0", "esbuild": "^0.17.14", "keytar": "^7.9.0", "prettier": "^3.2.5" diff --git a/apps/cli/src/apis.ts b/apps/cli/src/apis.ts index 8e6b5f6dd..73642ba64 100644 --- a/apps/cli/src/apis.ts +++ b/apps/cli/src/apis.ts @@ -1,52 +1,50 @@ -import type { - CodemodDownloadLinkResponse, - CodemodListResponse, - GetScopedTokenResponse, - GetUserDataResponse, - VerifyTokenResponse, +import { + type CodemodDownloadLinkResponse, + type CodemodListResponse, + type GetScopedTokenResponse, + type GetUserDataResponse, + type VerifyTokenResponse, + extendedFetch, } from "@codemod-com/utilities"; -import Axios, { type RawAxiosRequestHeaders } from "axios"; import type FormData from "form-data"; -export const getCLIAccessToken = async ( - accessToken: string, -): Promise<GetScopedTokenResponse> => { +export const getCLIAccessToken = async (accessToken: string) => { const url = new URL(`${process.env.AUTH_BACKEND_URL}/appToken`); - const res = await Axios.get<GetScopedTokenResponse>(url.toString(), { + const response = await extendedFetch(url.toString(), { + method: "GET", headers: { Authorization: `Bearer ${accessToken}` }, - timeout: 10000, + signal: AbortSignal.timeout(10000), }); - return res.data; + return (await response.json()) as GetScopedTokenResponse; }; -export const validateCLIToken = async ( - accessToken: string, -): Promise<VerifyTokenResponse> => { - const res = await Axios.get<VerifyTokenResponse>( +export const validateCLIToken = async (accessToken: string) => { + const response = await extendedFetch( `${process.env.AUTH_BACKEND_URL}/verifyToken`, { + method: "GET", headers: { Authorization: `Bearer ${accessToken}` }, - timeout: 5000, + signal: AbortSignal.timeout(10000), }, ); - return res.data; + return (await response.json()) as VerifyTokenResponse; }; -export const getUserData = async ( - accessToken: string, -): Promise<GetUserDataResponse | null> => { +export const getUserData = async (accessToken: string) => { try { - const { data } = await Axios.get<GetUserDataResponse | object>( + const response = await extendedFetch( `${process.env.AUTH_BACKEND_URL}/userData`, { headers: { Authorization: `Bearer ${accessToken}` }, - timeout: 5000, + signal: AbortSignal.timeout(5000), }, ); + const data = (await response.json()) as GetUserDataResponse; + if (!("user" in data)) { return null; } @@ -57,31 +55,25 @@ export const getUserData = async ( } }; -export const publish = async ( - accessToken: string, - formData: FormData, -): Promise<void> => { - await Axios.post(`${process.env.BACKEND_URL}/publish`, formData, { +export const publish = async (accessToken: string, formData: FormData) => { + await extendedFetch(`${process.env.BACKEND_URL}/publish`, { + method: "POST", + body: formData as any, headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "multipart/form-data", }, - timeout: 10000, + signal: AbortSignal.timeout(10000), }); }; -export const unpublish = async ( - accessToken: string, - name: string, -): Promise<void> => { - await Axios.post( - `${process.env.BACKEND_URL}/unpublish`, - { name }, - { - headers: { Authorization: `Bearer ${accessToken}` }, - timeout: 10000, - }, - ); +export const unpublish = async (accessToken: string, name: string) => { + await extendedFetch(`${process.env.BACKEND_URL}/unpublish`, { + method: "POST", + body: JSON.stringify({ name }), + headers: { Authorization: `Bearer ${accessToken}` }, + signal: AbortSignal.timeout(10000), + }); }; // @TODO @@ -96,34 +88,34 @@ export const revokeCLIToken = async (accessToken: string): Promise<void> => { export const getCodemodDownloadURI = async ( name: string, accessToken?: string, -): Promise<CodemodDownloadLinkResponse> => { +) => { const url = new URL(`${process.env.BACKEND_URL}/codemods/downloadLink`); if (name) { url.searchParams.set("name", name); } - const headers: RawAxiosRequestHeaders = {}; + const headers = new Headers(); if (accessToken) { - headers.Authorization = `Bearer ${accessToken}`; + headers.set("Authorization", `Bearer ${accessToken}`); } - const res = await Axios.get<CodemodDownloadLinkResponse>(url.toString(), { + const response = await extendedFetch(url.toString(), { headers, - timeout: 10000, + signal: AbortSignal.timeout(10000), }); - return res.data; + return (await response.json()) as CodemodDownloadLinkResponse; }; export const getCodemodList = async (options?: { accessToken?: string; search?: string | null; -}): Promise<CodemodListResponse> => { +}) => { const { accessToken, search } = options ?? {}; - const headers: RawAxiosRequestHeaders = {}; + const headers = new Headers(); if (accessToken) { - headers.Authorization = `Bearer ${accessToken}`; + headers.set("Authorization", `Bearer ${accessToken}`); } const url = new URL(`${process.env.BACKEND_URL}/codemods/list`); @@ -131,40 +123,40 @@ export const getCodemodList = async (options?: { url.searchParams.set("search", search); } - const res = await Axios.get<CodemodListResponse>(url.toString(), { + const response = await extendedFetch(url.toString(), { headers, - timeout: 10000, + signal: AbortSignal.timeout(10000), }); - return res.data; + return (await response.json()) as CodemodListResponse; }; type UserLoginIntentResponse = { id: string; iv: string; }; -export const generateUserLoginIntent = - async (): Promise<UserLoginIntentResponse> => { - const res = await Axios.post<UserLoginIntentResponse>( - `${process.env.AUTH_BACKEND_URL}/intents`, - {}, - ); +export const generateUserLoginIntent = async () => { + const response = await extendedFetch( + `${process.env.AUTH_BACKEND_URL}/intents`, + { + method: "POST", + body: JSON.stringify({}), + }, + ); - return res.data; - }; + return (await response.json()) as UserLoginIntentResponse; +}; type ConfirmUserLoggedInResponse = { token: string; }; -export const confirmUserLoggedIn = async ( - sessionId: string, - iv: string, -): Promise<string> => { - const res = await Axios.get<ConfirmUserLoggedInResponse>( +export const confirmUserLoggedIn = async (sessionId: string, iv: string) => { + const response = await extendedFetch( `${process.env.AUTH_BACKEND_URL}/intents/${sessionId}?iv=${iv}`, ); - return res.data.token; + const data = (await response.json()) as ConfirmUserLoggedInResponse; + return data.token; }; type CreateCodeDiffResponse = { @@ -174,15 +166,14 @@ type CreateCodeDiffResponse = { export const createCodeDiff = async (body: { beforeSnippet: string; afterSnippet: string; -}): Promise<CreateCodeDiffResponse> => { - const res = await Axios.post<CreateCodeDiffResponse>( - `${process.env.BACKEND_URL}/diffs`, - { +}) => { + const response = await extendedFetch(`${process.env.BACKEND_URL}/diffs`, { + method: "POST", + body: JSON.stringify({ before: body.beforeSnippet, after: body.afterSnippet, source: "cli", - }, - ); - - return res.data; + }), + }); + return (await response.json()) as CreateCodeDiffResponse; }; diff --git a/apps/cli/src/buildCodemodOptions.ts b/apps/cli/src/buildCodemodOptions.ts index c94eb334b..5e141941b 100644 --- a/apps/cli/src/buildCodemodOptions.ts +++ b/apps/cli/src/buildCodemodOptions.ts @@ -6,13 +6,13 @@ import type { Codemod, CodemodSettings } from "@codemod-com/runner"; import { type AllEngines, type CodemodConfig, + FetchError, type FileSystem, type TarService, allEnginesSchema, doubleQuotify, parseCodemodConfig, } from "@codemod-com/utilities"; -import { AxiosError } from "axios"; import unzipper from "unzipper"; import { object, parse } from "valibot"; import type { CodemodDownloaderBlueprint } from "./downloadCodemod.js"; @@ -184,10 +184,10 @@ export const buildSourcedCodemodOptions = async ( return await codemodDownloader.download(subCodemodName, true); } catch (error) { spinner.fail(); - if (error instanceof AxiosError) { + if (error instanceof FetchError) { if ( error.response?.status === 400 && - error.response.data.error === "Codemod not found" + (await error.response.json()).error === "Codemod not found" ) { throw new Error( `Error locating one of the recipe codemods: ${chalk.bold( diff --git a/apps/cli/src/commands/publish.ts b/apps/cli/src/commands/publish.ts index 5cd5b966c..85abc7ac0 100644 --- a/apps/cli/src/commands/publish.ts +++ b/apps/cli/src/commands/publish.ts @@ -2,12 +2,11 @@ import * as fs from "node:fs"; import { basename, dirname, join, resolve } from "node:path"; import { type PrinterBlueprint, chalk } from "@codemod-com/printer"; import { + FetchError, codemodNameRegex, doubleQuotify, - execPromise, parseCodemodConfig, } from "@codemod-com/utilities"; -import { AxiosError } from "axios"; import FormData from "form-data"; import { glob } from "glob"; import inquirer from "inquirer"; @@ -263,7 +262,9 @@ export const handlePublishCliCommand = async (options: { } catch (error) { publishSpinner.fail(); const message = - error instanceof AxiosError ? error.response?.data.error : String(error); + error instanceof FetchError + ? ((await error.response?.json()) as any).error + : String(error); const errorMessage = `${chalk.bold( `Could not publish the "${codemodRc.name}" codemod`, )}:\n${message}`; diff --git a/apps/cli/src/commands/run.ts b/apps/cli/src/commands/run.ts index 2441f4ba9..59220ca35 100644 --- a/apps/cli/src/commands/run.ts +++ b/apps/cli/src/commands/run.ts @@ -14,13 +14,13 @@ import { } from "@codemod-com/runner"; import type { TelemetrySender } from "@codemod-com/telemetry"; import { + FetchError, TarService, doubleQuotify, execPromise, parseCodemodConfig, sleep, } from "@codemod-com/utilities"; -import { AxiosError } from "axios"; import inquirer from "inquirer"; import type { TelemetryEvent } from "../analytics/telemetry.js"; import { buildSourcedCodemodOptions } from "../buildCodemodOptions.js"; @@ -159,10 +159,10 @@ export const handleRunCliCommand = async (options: { try { codemod = await codemodDownloader.download(codemodSettings.name); } catch (error) { - if (error instanceof AxiosError) { + if (error instanceof FetchError) { if ( error.response?.status === 400 && - error.response.data.error === "Codemod not found" + (await error.response.json()).error === "Codemod not found" ) { printer.printConsoleMessage( "error", diff --git a/apps/cli/src/commands/unpublish.ts b/apps/cli/src/commands/unpublish.ts index 3f466de36..c2bdf8984 100644 --- a/apps/cli/src/commands/unpublish.ts +++ b/apps/cli/src/commands/unpublish.ts @@ -1,10 +1,10 @@ import { type PrinterBlueprint, chalk } from "@codemod-com/printer"; import { + FetchError, doubleQuotify, extractLibNameAndVersion, isNeitherNullNorUndefined, } from "@codemod-com/utilities"; -import { AxiosError } from "axios"; import { unpublish } from "../apis.js"; import { getCurrentUserData } from "../utils.js"; @@ -49,7 +49,9 @@ export const handleUnpublishCliCommand = async (options: { } catch (error) { spinner.fail(); const message = - error instanceof AxiosError ? error.response?.data.error : String(error); + error instanceof FetchError + ? ((await error.response?.json()) as any).error + : String(error); const errorMessage = `${chalk.bold( `Could not unpublish the "${name}" codemod`, )}:\n${message}`; diff --git a/apps/cli/src/downloadCodemod.ts b/apps/cli/src/downloadCodemod.ts index c6840ee34..3690f664b 100644 --- a/apps/cli/src/downloadCodemod.ts +++ b/apps/cli/src/downloadCodemod.ts @@ -3,14 +3,16 @@ import { mkdir, readFile } from "node:fs/promises"; import { join } from "node:path"; import { type PrinterBlueprint, chalk } from "@codemod-com/printer"; import type { Codemod } from "@codemod-com/runner"; -import type { CodemodDownloadLinkResponse } from "@codemod-com/utilities"; +import type { + CodemodDownloadLinkResponse, + FetchError, +} from "@codemod-com/utilities"; import { type CodemodConfig, doubleQuotify, parseCodemodConfig, } from "@codemod-com/utilities"; import type { TarService } from "@codemod-com/utilities"; -import type { AxiosError } from "axios"; import inquirer from "inquirer"; import semver from "semver"; import { getCodemodDownloadURI } from "./apis.js"; @@ -68,7 +70,7 @@ export class CodemodDownloader implements CodemodDownloaderBlueprint { } catch (err) { spinner?.fail(); throw new Error( - (err as AxiosError<{ error: string }>).response?.data?.error ?? + ((await (err as FetchError).response?.json()) as any).error ?? "Error getting download link for codemod", ); } @@ -86,7 +88,7 @@ export class CodemodDownloader implements CodemodDownloaderBlueprint { } catch (err) { spinner?.fail(); throw new Error( - (err as AxiosError<{ error: string }>).response?.data?.error ?? + ((await (err as FetchError).response?.json()) as any).error ?? "Error downloading codemod from the registry", ); } diff --git a/apps/cli/src/executeMainThread.ts b/apps/cli/src/executeMainThread.ts index 360975521..caee54e5b 100644 --- a/apps/cli/src/executeMainThread.ts +++ b/apps/cli/src/executeMainThread.ts @@ -4,8 +4,11 @@ import { PostHogSender, type TelemetrySender, } from "@codemod-com/telemetry"; -import { doubleQuotify, execPromise } from "@codemod-com/utilities"; -import Axios from "axios"; +import { + addGlobalHook, + doubleQuotify, + execPromise, +} from "@codemod-com/utilities"; import semver from "semver"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; @@ -68,10 +71,20 @@ const initializeDependencies = async (argv: { const clientIdentifier = typeof argv.clientIdentifier === "string" ? argv.clientIdentifier : "CLI"; - Axios.interceptors.request.use((config) => { - config.headers["X-Client-Identifier"] = clientIdentifier; + addGlobalHook((options) => { + options.headers = options.headers ?? {}; + + if (options.headers) { + if (options.headers instanceof Headers) { + options.headers.set("X-Client-Identifier", clientIdentifier); + } else if (Array.isArray(options.headers)) { + options.headers.push(["X-Client-Identifier", clientIdentifier]); + } else { + options.headers["X-Client-Identśifier"] = clientIdentifier; + } + } - return config; + return options; }); const printer = new Printer(argv.json); diff --git a/apps/cli/src/fileDownloadService.ts b/apps/cli/src/fileDownloadService.ts index 70d885381..8a4116b80 100644 --- a/apps/cli/src/fileDownloadService.ts +++ b/apps/cli/src/fileDownloadService.ts @@ -1,6 +1,9 @@ import type { PrinterBlueprint } from "@codemod-com/printer"; -import type { FileSystem } from "@codemod-com/utilities"; -import axios, { isAxiosError, type AxiosResponse } from "axios"; +import { + type FileSystem, + extendedFetch, + isFetchError, +} from "@codemod-com/utilities"; export type FileDownloadServiceBlueprint = Readonly<{ cacheEnabled: boolean; @@ -43,7 +46,8 @@ export class FileDownloadService implements FileDownloadServiceBlueprint { } } - const response = await fetch(url);if (!response.ok) throw new Error('Network response was not ok.');const data = await response.arrayBuffer(); + const response = await extendedFetch(url); + const data = await response.arrayBuffer(); const buffer = Buffer.from(data); @@ -65,12 +69,14 @@ export class FileDownloadService implements FileDownloadServiceBlueprint { private async __getRemoteFileLastModified( url: string, ): Promise<number | null> { - let response: AxiosResponse; + let response: Response; try { - response = await fetch(url, { signal: AbortSignal.timeout(15000) })if (!response.ok) throw new Error('Network response was not ok.');; + response = await extendedFetch(url, { + signal: AbortSignal.timeout(15000), + }); } catch (error) { - if (!isAxiosError(error)) { + if (!isFetchError(error)) { throw error; } @@ -79,11 +85,10 @@ export class FileDownloadService implements FileDownloadServiceBlueprint { if (status === 403) { return null; } - return null; } - const lastModified = response.headers["last-modified"]; + const lastModified = response.headers.get("last-modified"); return lastModified ? Date.parse(lastModified) : null; } diff --git a/apps/frontend/app/(website)/studio/src/api/getCodeDiff.ts b/apps/frontend/app/(website)/studio/src/api/getCodeDiff.ts index c8a955fde..b512b9549 100644 --- a/apps/frontend/app/(website)/studio/src/api/getCodeDiff.ts +++ b/apps/frontend/app/(website)/studio/src/api/getCodeDiff.ts @@ -12,11 +12,13 @@ export const getCodeDiff = async (body: { const { diffId, iv } = body; try { - const res = await apiClient.get<GetCodeDiffResponse>( - `diffs/${diffId}?iv=${iv}`, - ); + const response = await apiClient(`diffs/${diffId}?iv=${iv}`, { + headers: { + "Content-Type": "application/json", + }, + }); - return res.data; + return (await response.json()) as GetCodeDiffResponse; } catch (e) { console.error(e); return null; diff --git a/apps/frontend/app/(website)/studio/src/api/populateLoginIntent.ts b/apps/frontend/app/(website)/studio/src/api/populateLoginIntent.ts index f5d7d5ed4..42b762e61 100644 --- a/apps/frontend/app/(website)/studio/src/api/populateLoginIntent.ts +++ b/apps/frontend/app/(website)/studio/src/api/populateLoginIntent.ts @@ -1,6 +1,6 @@ import { authApiClient } from "@/utils/apis/client"; +import type { FetchError } from "@codemod-com/utilities"; import { isNeitherNullNorUndefined } from "@studio/utils/isNeitherNullNorUndefined"; -import type { AxiosError } from "axios"; import { POPULATE_LOGIN_INTENT } from "../constants"; import { Either } from "../utils/Either"; @@ -38,7 +38,12 @@ export const populateLoginIntent = async ({ return Either.right(accessToken); } catch (e) { - const err = e as AxiosError<{ message?: string }>; - return Either.left(new Error(err.response?.data.message ?? err.message)); + const err = e as FetchError; + return Either.left( + new Error( + ((await err.response?.json()) as { message?: string }).message ?? + err.message, + ), + ); } }; diff --git a/apps/frontend/app/(website)/studio/src/api/sendMessage.ts b/apps/frontend/app/(website)/studio/src/api/sendMessage.ts index 569a15801..05a76aa6a 100644 --- a/apps/frontend/app/(website)/studio/src/api/sendMessage.ts +++ b/apps/frontend/app/(website)/studio/src/api/sendMessage.ts @@ -1,5 +1,5 @@ import { apiClient } from "@/utils/apis/client"; -import type { AxiosError } from "axios"; +import type { FetchError } from "@codemod-com/utilities"; import { SEND_MESSAGE } from "../constants"; import { Either } from "../utils/Either"; @@ -22,23 +22,26 @@ const sendMessage = async ({ token, }: SendMessageRequest): Promise<Either<Error, SendMessageResponse>> => { try { - const res = await apiClient.post<SendMessageResponse>( - SEND_MESSAGE, - { + const response = await apiClient(SEND_MESSAGE, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ message, parentMessageId, - }, - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ); + }), + }); - return Either.right(res.data); + return Either.right((await response.json()) as SendMessageResponse); } catch (e) { - const err = e as AxiosError<{ message?: string }>; - return Either.left(new Error(err.response?.data.message ?? err.message)); + const err = e as FetchError; + return Either.left( + new Error( + ((await err.response?.json()) as { message?: string }).message ?? + err.message, + ), + ); } }; diff --git a/apps/frontend/app/(website)/studio/src/hooks/useAPI.ts b/apps/frontend/app/(website)/studio/src/hooks/useAPI.ts index 445751d09..8a5feee3d 100644 --- a/apps/frontend/app/(website)/studio/src/hooks/useAPI.ts +++ b/apps/frontend/app/(website)/studio/src/hooks/useAPI.ts @@ -2,14 +2,13 @@ import { apiClient } from "@/utils/apis/client"; import { useAuth } from "@clerk/nextjs"; import { mockedEndpoints } from "@shared/mocks"; import { isServer } from "@studio/config"; -import type { AxiosResponse } from "axios"; const shouldUseMocks = !isServer && process.env.NODE_ENV === "development" && Boolean(localStorage?.getItem("useMocks")); const mockified = ( - verb: "put" | "get" | "post", + verb: "PUT" | "GET" | "POST", endpoint: string | ((x: any) => string), ...rest: any[] ) => { @@ -20,8 +19,7 @@ const mockified = ( const response = mockedEndpoints[path][verb](...rest); return new Promise((r) => setTimeout(() => r(response), 1000)); } - // @ts-ignore - return apiClient[verb](...rest); + return apiClient(path, { method: verb, ...rest }); }; export const useAPI = <T>(endpoint: string) => { @@ -39,21 +37,32 @@ export const useAPI = <T>(endpoint: string) => { return { get: async <U = T>() => await (shouldUseMocks - ? (mockified("get", endpoint, await getHeaders()) as Promise< - AxiosResponse<U> - >) - : apiClient.get<U>(endpoint, await getHeaders())), + ? (mockified("GET", endpoint, await getHeaders()) as Promise<U>) + : (( + await apiClient(endpoint, { + method: "GET", + ...(await getHeaders()), + }) + ).json() as Promise<U>)), put: async <U = T>(body: U) => await (shouldUseMocks - ? (mockified("put", endpoint, body, await getHeaders()) as Promise< - AxiosResponse<U> - >) - : apiClient.put<U>(endpoint, body, await getHeaders())), + ? (mockified("PUT", endpoint, body, await getHeaders()) as Promise<U>) + : (( + await apiClient(endpoint, { + method: "PUT", + body: JSON.stringify(body), + ...(await getHeaders()), + }) + ).json() as Promise<U>)), post: async <U, K = T>(body: U) => await (shouldUseMocks - ? (mockified("post", endpoint, body, await getHeaders()) as Promise< - AxiosResponse<K> - >) - : apiClient.post<K>(endpoint, body, await getHeaders())), + ? (mockified("POST", endpoint, body, await getHeaders()) as Promise<K>) + : (( + await apiClient(endpoint, { + method: "POST", + body: JSON.stringify(body), + ...(await getHeaders()), + }) + ).json() as Promise<K>)), }; }; diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 04f078624..e34d642b9 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -71,7 +71,6 @@ "ai": "^2.1.32", "ast-node-builder": "^4.2.1", "ast-types": "^0.14.2", - "axios": "^1.6.8", "change-case": "^5.2.0", "chart.js": "^4.4.2", "class-variance-authority": "^0.7.0", diff --git a/apps/frontend/utils/apis/client.ts b/apps/frontend/utils/apis/client.ts index 62c9e6e7a..9353ef8f8 100644 --- a/apps/frontend/utils/apis/client.ts +++ b/apps/frontend/utils/apis/client.ts @@ -1,37 +1,32 @@ import { env } from "@/env"; -import axios, { type AxiosError } from "axios"; +import { extendedFetch, isFetchError } from "@codemod-com/utilities"; import toast from "react-hot-toast"; -const apiClient = axios.create({ - baseURL: env.NEXT_PUBLIC_API_URL, - timeout: 60000, -}); +const createClient = + (baseUrl: string) => async (path: string, options?: RequestInit) => { + try { + return await extendedFetch(`${baseUrl}${path}`, { + signal: AbortSignal.timeout(60000), + ...options, + }); + } catch (error) { + if (isFetchError(error) && error.response?.status) { + toast.error( + ((await error.response.json()) as { message?: string })?.message ?? + "Network Error", + { + position: "top-center", + }, + ); + } + throw error; + } + }; + +const apiClient = createClient(env.NEXT_PUBLIC_API_URL); // mostly for local dev, in prod they should be on the same domain. // later we need to figure out how to do this in a better way -const authApiClient = axios.create({ - baseURL: env.NEXT_PUBLIC_AUTH_API_URL, - timeout: 60000, -}); - -const errorHandler = (error: AxiosError<{ message?: string }>) => { - if (error.response?.status) { - toast.error(error.response?.data.message ?? "Network Error", { - position: "top-center", - }); - } - - return Promise.reject({ ...error }); -}; - -apiClient.interceptors.response.use( - (response) => response, - (error) => errorHandler(error), -); - -authApiClient.interceptors.response.use( - (response) => response, - (error) => errorHandler(error), -); +const authApiClient = createClient(env.NEXT_PUBLIC_AUTH_API_URL); export { apiClient, authApiClient }; diff --git a/apps/modgpt/package.json b/apps/modgpt/package.json index 97876ca9c..ebe689b9c 100644 --- a/apps/modgpt/package.json +++ b/apps/modgpt/package.json @@ -14,7 +14,6 @@ "@fastify/multipart": "^8.1.0", "@fastify/rate-limit": "9.0.1", "ai": "2.2.29", - "axios": "^1.6.8", "chatgpt": "5.2.5", "dotenv": "^16.4.5", "fastify": "4.25.1", diff --git a/apps/modgpt/src/plugins/authPlugin.ts b/apps/modgpt/src/plugins/authPlugin.ts index aa05eb427..cd6d17529 100644 --- a/apps/modgpt/src/plugins/authPlugin.ts +++ b/apps/modgpt/src/plugins/authPlugin.ts @@ -1,5 +1,8 @@ -import type { OrganizationMembership, User } from "@codemod-com/utilities"; -import axios from "axios"; +import { + type OrganizationMembership, + type User, + extendedFetch, +} from "@codemod-com/utilities"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import fp from "fastify-plugin"; import { environment } from "../dev-utils/configs"; @@ -30,9 +33,15 @@ async function authPlugin(fastify: FastifyInstance, _opts: unknown) { try { const authHeader = request.headers.authorization; - if (!authHeader) reply.code(401).send({ error: "Unauthorized" }); + if (!authHeader) { + reply.code(401).send({ error: "Unauthorized" }); + return; + } - await const controller = new AbortController();setTimeout(() => controller.abort(), 5000); // Assuming a default timeout of 5000ms as it was not specified in the original axios callconst response = await fetch(`${environment.AUTH_SERVICE_URL}/verifyToken`, { headers: { Authorization: authHeader, }, signal: controller.signal});if (!response.ok) throw new Error('Failed to fetch');const result = { data: await response.json() };; + await extendedFetch(`${environment.AUTH_SERVICE_URL}/verifyToken`, { + headers: { Authorization: authHeader }, + signal: AbortSignal.timeout(5000), + }); } catch (error) { console.log(error); } diff --git a/apps/shared/mocks/gh-run.ts b/apps/shared/mocks/gh-run.ts index df0c4ca7e..f99973176 100644 --- a/apps/shared/mocks/gh-run.ts +++ b/apps/shared/mocks/gh-run.ts @@ -105,33 +105,27 @@ export const mockGHBranches: GHBranch[] = [ ]; export const mockedGhRunEndpoints = { [GH_REPO_LIST]: { - get: (): { data: GithubRepository[] } => ({ data: mockGithubRepositories }), + GET: (): GithubRepository[] => mockGithubRepositories, }, [GH_BRANCH_LIST]: { - post: ({ repoUrl }: { repoUrl: string }): { data: GHBranch[] } => { + POST: ({ repoUrl }: { repoUrl: string }): GHBranch[] => { isSuccess = repoUrl === "success"; - return { - data: mockGHBranches, - }; + return mockGHBranches; }, }, [RUN_CODEMOD]: { - post: (): { data: CodemodRunStatus } => ({ - data: { codemodRunId: "1", success: true }, - }), + POST: (): CodemodRunStatus => ({ codemodRunId: "1", success: true }), }, [GET_EXECUTION_STATUS("1")]: { - get: (): { data: GetExecutionStatusResponse } => { + GET: (): GetExecutionStatusResponse => { const index = executionResultsIndex; executionResultsIndex = executionResultsIndex === getExecutionResults().length ? 0 : executionResultsIndex + 1; return { - data: { - result: getExecutionResults()[index] || null, - success: true, - }, + result: getExecutionResults()[index] || null, + success: true, }; }, }, diff --git a/apps/task-manager/package.json b/apps/task-manager/package.json index a2bb5e2ac..c99fc44ff 100644 --- a/apps/task-manager/package.json +++ b/apps/task-manager/package.json @@ -8,14 +8,14 @@ "author": "Codemod inc.", "private": true, "dependencies": { - "axios": "^1.6.8", "bullmq": "^5.7.2", "dotenv": "^16.4.5", "ioredis": "^5.4.1", "parse-github-url": "1.0.2", "simple-git": "^3.24.0", "uuid": "^10.0.0", - "valibot": "^0.24.1" + "valibot": "^0.24.1", + "@codemod-com/utilities": "workspace:*" }, "type": "module", "devDependencies": { diff --git a/apps/task-manager/src/services/Auth.ts b/apps/task-manager/src/services/Auth.ts index 12020be75..e0f6a2432 100644 --- a/apps/task-manager/src/services/Auth.ts +++ b/apps/task-manager/src/services/Auth.ts @@ -1,47 +1,44 @@ -import axios, { isAxiosError } from 'axios'; +import { extendedFetch } from "@codemod-com/utilities"; export class AuthError extends Error {} const USER_ID_REGEX = /^[a-z0-9_]+$/i; export class AuthService { - private readonly __authHeader: string; - - constructor(authKey: string) { - if (!authKey) { - throw new AuthError('Invalid auth key provided.'); - } - this.__authHeader = `Bearer ${authKey}`; - } - - async getAuthToken(userId: string): Promise<string> { - try { - if (!USER_ID_REGEX.test(userId)) { - throw new AuthError('Invalid userId.'); - } - - const response = await fetch( - `https://api.clerk.dev/v1/users/${userId}/oauth_access_tokens/github`, - { headers: { Authorization: this.__authHeader } }, - ); - if (!response.ok) { - throw new Error('Failed to fetch data'); - } - const result = { data: await response.json() }; - - const token = result.data[0]?.token; - - if (!token) { - throw new AuthError('Missing OAuth token'); - } - - return token; - } catch (error) { - const { message } = error as Error; - - throw new AuthError( - `Failed to retrieve OAuth token for GitHub. Reason: ${message}`, - ); - } - } + private readonly __authHeader: string; + + constructor(authKey: string) { + if (!authKey) { + throw new AuthError("Invalid auth key provided."); + } + this.__authHeader = `Bearer ${authKey}`; + } + + async getAuthToken(userId: string): Promise<string> { + try { + if (!USER_ID_REGEX.test(userId)) { + throw new AuthError("Invalid userId."); + } + + const response = await extendedFetch( + `https://api.clerk.dev/v1/users/${userId}/oauth_access_tokens/github`, + { headers: { Authorization: this.__authHeader } }, + ); + const result = { data: (await response.json()) as { token: string }[] }; + + const token = result.data[0]?.token; + + if (!token) { + throw new AuthError("Missing OAuth token"); + } + + return token; + } catch (error) { + const { message } = error as Error; + + throw new AuthError( + `Failed to retrieve OAuth token for GitHub. Reason: ${message}`, + ); + } + } } diff --git a/apps/task-manager/src/services/GithubProvider.ts b/apps/task-manager/src/services/GithubProvider.ts index 2255db769..b2d1ae9cd 100644 --- a/apps/task-manager/src/services/GithubProvider.ts +++ b/apps/task-manager/src/services/GithubProvider.ts @@ -1,6 +1,7 @@ +import { extendedFetch } from "@codemod-com/utilities"; import { type SimpleGit, simpleGit } from "simple-git"; import type { CodemodMetadata } from "../jobs/runCodemod"; -import { axiosRequest, parseGithubRepoUrl } from "../util"; +import { parseGithubRepoUrl } from "../util"; const BASE_URL = "https://api.github.com"; @@ -116,20 +117,22 @@ export class GithubProviderService { const title = `[${codemodName}]: Codemod changes for ${authorName}/${repoName}.`; const body = `Changes applied with ${codemodName} codemod.`; - const pullRequestResponse = await axiosRequest<PullRequestResponse>( - url, - "post", - { + const response = await extendedFetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${this.__codemodMetadata.token}`, + Accept: "application/vnd.github+json", + }, + body: JSON.stringify({ head: this.__currentBranch, base: branch ?? this.__base, title, body, - }, - { - Authorization: `Bearer ${this.__codemodMetadata.token}`, - Accept: "application/vnd.github+json", - }, - ); + }), + }); + + const pullRequestResponse = + (await response.json()) as PullRequestResponse; return pullRequestResponse.html_url; } catch (error) { diff --git a/apps/task-manager/src/util.ts b/apps/task-manager/src/util.ts index 1135c7495..66dc0437e 100644 --- a/apps/task-manager/src/util.ts +++ b/apps/task-manager/src/util.ts @@ -1,62 +1,26 @@ -import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'; -import gh from 'parse-github-url'; +import gh from "parse-github-url"; -import { parseEnvironment } from './schemata/env.js'; +import { parseEnvironment } from "./schemata/env.js"; export const environment = parseEnvironment(process.env); class InvalidGithubUrlError extends Error {} class ParseGithubUrlError extends Error {} -class AxiosRequestError extends Error {} type Repository = { - authorName: string; - repoName: string; + authorName: string; + repoName: string; }; export function parseGithubRepoUrl(url: string): Repository { - try { - const { owner, name } = gh(url) ?? {}; - if (!owner || !name) - throw new InvalidGithubUrlError('Missing owner or name'); - - return { authorName: owner, repoName: name }; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - throw new ParseGithubUrlError(errorMessage); - } -} - -export async function axiosRequest<T>( - url: string, - method: 'get' | 'post' | 'put' | 'delete', - data: Record<string, unknown> | null, - headers?: Record<string, string>, -): Promise<T> { - try { - const config: AxiosRequestConfig = { - url, - method, - data, - headers, - }; - - const res: AxiosResponse<T> = await fetch(config.url, { - method: config.method, - headers: config.headers, - body: JSON.stringify(config.data), - signal: config.timeout - ? AbortSignal.timeout(config.timeout) - : undefined, - }).then((response) => { - if (!response.ok) { - throw new AxiosRequestError(); - } - return response.json().then((data) => ({ data })); - }); - return res.data; - } catch (error) { - throw new AxiosRequestError(); - } + try { + const { owner, name } = gh(url) ?? {}; + if (!owner || !name) + throw new InvalidGithubUrlError("Missing owner or name"); + + return { authorName: owner, repoName: name }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new ParseGithubUrlError(errorMessage); + } } diff --git a/apps/vsce/package.json b/apps/vsce/package.json index b4be81fa1..f69f0a706 100644 --- a/apps/vsce/package.json +++ b/apps/vsce/package.json @@ -223,8 +223,6 @@ "@codemod-com/telemetry": "workspace:*", "@reduxjs/toolkit": "^1.9.5", "@vscode/vsce": "^2.22.0", - "axios": "^1.6.8", - "axios-retry": "^4.0.0", "diff": "^5.1.0", "fast-deep-equal": "^3.1.3", "fp-ts": "^2.15.0", @@ -235,7 +233,7 @@ "io-ts-types": "^0.5.19", "monocle-ts": "^2.3.13", "newtype-ts": "^0.3.5", - "nock": "^13.5.1", + "nock": "beta", "redux-persist": "^6.0.0", "semver": "^7.3.8", "ts-morph": "^19.0.0", diff --git a/apps/vsce/src/axios/index.ts b/apps/vsce/src/axios/index.ts deleted file mode 100644 index c0d737bd5..000000000 --- a/apps/vsce/src/axios/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import axios from "axios"; -import axiosRetry from "axios-retry"; - -const retryingClient = axios.create(); -axiosRetry(retryingClient); - -export const DEFAULT_RETRY_COUNT = 3; -export { retryingClient }; diff --git a/apps/vsce/src/components/downloadService.ts b/apps/vsce/src/components/downloadService.ts index 70fb205fe..0c60459a5 100644 --- a/apps/vsce/src/components/downloadService.ts +++ b/apps/vsce/src/components/downloadService.ts @@ -1,7 +1,7 @@ import type { Mode } from "node:fs"; -import { type AxiosResponse, isAxiosError } from "axios"; +import { isFetchError } from "@codemod-com/utilities"; import type { FileSystem, Uri } from "vscode"; -import { DEFAULT_RETRY_COUNT, retryingClient } from "../axios"; +import { DEFAULT_RETRY_COUNT, retryingClient } from "../fetch"; import type { FileSystemUtilities } from "./fileSystemUtilities"; export class RequestError extends Error {} @@ -27,21 +27,20 @@ export class DownloadService { const localModificationTime = await this.#fileSystemUtilities.getModificationTime(uri); - let response: AxiosResponse | undefined; + let response: Response | undefined; try { - response = await retryingClient.head(url, { - timeout: 15000, - "axios-retry": { - retries: DEFAULT_RETRY_COUNT, - }, + response = await retryingClient(url, { + method: "HEAD", + retries: DEFAULT_RETRY_COUNT, + signal: AbortSignal.timeout(15000), }); } catch (error) { if (localModificationTime > 0) { return false; } - if (!isAxiosError(error)) { + if (!isFetchError(error)) { throw error; } @@ -56,7 +55,7 @@ export class DownloadService { throw new RequestError(`Could not make a request to ${url}`); } - const lastModified = response?.headers["last-modified"] ?? null; + const lastModified = response?.headers.get("last-modified") ?? null; const remoteModificationTime = lastModified ? Date.parse(lastModified) : localModificationTime; @@ -75,13 +74,11 @@ export class DownloadService { uri: Uri, chmod: Mode | null, ): Promise<void> { - const response = await retryingClient.get(url, { - responseType: "arraybuffer", - "axios-retry": { - retries: DEFAULT_RETRY_COUNT, - }, + const response = await retryingClient(url, { + retries: DEFAULT_RETRY_COUNT, + signal: AbortSignal.timeout(15000), }); - const content = new Uint8Array(response.data); + const content = new Uint8Array(await response.arrayBuffer()); await this.#fileSystem.writeFile(uri, content); diff --git a/apps/vsce/src/components/engineService.ts b/apps/vsce/src/components/engineService.ts index e1035cd75..c6bbb77b1 100644 --- a/apps/vsce/src/components/engineService.ts +++ b/apps/vsce/src/components/engineService.ts @@ -9,7 +9,7 @@ import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; import { join } from "node:path"; import * as readline from "node:readline"; -import axios from "axios"; +import { extendedFetch } from "@codemod-com/utilities"; import * as E from "fp-ts/Either"; import { type FileSystem, Uri, commands, window } from "vscode"; import type { Case } from "../cases/types"; @@ -80,14 +80,17 @@ const CODEMOD_ENGINE_NODE_COMMAND = "codemod"; const CODEMOD_ENGINE_NODE_POLLING_INTERVAL = 1250; const CODEMOD_ENGINE_NODE_POLLING_ITERATIONS_LIMIT = 200; -export const getCodemodList = async (): Promise<CodemodListResponse> => { +export const getCodemodList = async () => { const url = new URL("https://backend.codemod.com/codemods/list"); - const res = await axios.get<CodemodListResponse>(url.toString(), { - timeout: 10000, + const response = await extendedFetch(url.toString(), { + headers: { + "Content-Type": "application/json", + }, + signal: AbortSignal.timeout(10000), }); - return res.data; + return (await response.json()) as CodemodListResponse; }; const buildCodemodEntry = ( diff --git a/apps/vsce/src/components/webview/MainProvider.ts b/apps/vsce/src/components/webview/MainProvider.ts index fba6eaf35..f79c4d973 100644 --- a/apps/vsce/src/components/webview/MainProvider.ts +++ b/apps/vsce/src/components/webview/MainProvider.ts @@ -1,559 +1,528 @@ -import axios from 'axios'; -import areEqual from 'fast-deep-equal'; -import { glob } from 'glob'; +import { extendedFetch, isFetchError } from "@codemod-com/utilities"; +import areEqual from "fast-deep-equal"; +import { glob } from "glob"; import { - type ExtensionContext, - Uri, - type WebviewView, - type WebviewViewProvider, - commands, - window, - workspace, -} from 'vscode'; -import type { Store } from '../../data'; -import { actions } from '../../data/slice'; -import { createIssueResponseCodec } from '../../github/types'; + type ExtensionContext, + Uri, + type WebviewView, + type WebviewViewProvider, + commands, + window, + workspace, +} from "vscode"; +import type { Store } from "../../data"; +import { actions } from "../../data/slice"; +import { createIssueResponseCodec } from "../../github/types"; import { - type CodemodNodeHashDigest, - relativeToAbsolutePath, - selectCodemodArguments, -} from '../../selectors/selectCodemodTree'; -import { selectMainWebviewViewProps } from '../../selectors/selectMainWebviewViewProps'; -import { buildGlobPattern, isNeitherNullNorUndefined } from '../../utilities'; -import type { EngineService } from '../engineService'; -import { type MessageBus, MessageKind } from '../messageBus'; -import { WebviewResolver } from './WebviewResolver'; + type CodemodNodeHashDigest, + relativeToAbsolutePath, + selectCodemodArguments, +} from "../../selectors/selectCodemodTree"; +import { selectMainWebviewViewProps } from "../../selectors/selectMainWebviewViewProps"; +import { buildGlobPattern, isNeitherNullNorUndefined } from "../../utilities"; +import type { EngineService } from "../engineService"; +import { type MessageBus, MessageKind } from "../messageBus"; +import { WebviewResolver } from "./WebviewResolver"; import type { - CodemodHash, - WebviewMessage, - WebviewResponse, -} from './webviewEvents'; + CodemodHash, + WebviewMessage, + WebviewResponse, +} from "./webviewEvents"; export const validateAccessToken = async ( - accessToken: string, + accessToken: string, ): Promise<void> => { - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 5000); - const response = await fetch( - 'https://backend.codemod.com/verifyToken', - { - method: 'POST', - headers: { Authorization: `Bearer ${accessToken}` }, - signal: controller.signal, - }, - ); - clearTimeout(timeoutId); - if (!response.ok) throw new Error('Network response was not ok.'); - const data = await response.json(); - - return response.data; - } catch (error) { - if ((!error) instanceof Error && error.name === 'AbortError') { - console.error(error); - } - } + try { + const response = await extendedFetch( + "https://backend.codemod.com/verifyToken", + { + method: "POST", + headers: { Authorization: `Bearer ${accessToken}` }, + signal: AbortSignal.timeout(5000), + }, + ); + return await response.json(); + } catch (error) { + if (!isFetchError(error)) { + console.error(error); + } + } }; export const createIssue = async ( - title: string, - body: string, - accessToken: string, - onSuccess: () => void, - onFail: () => Promise<void>, + title: string, + body: string, + accessToken: string, + onSuccess: () => void, + onFail: () => Promise<void>, ): Promise<{ status: number; html_url: string | null }> => { - // call API to create Github Issue - const codemodRegistryRepoUrl = 'https://github.com/codemod-com/codemod'; - - const result = await fetch( - 'https://backend.codemod.com/sourceControl/github/issues', - { - method: 'POST', - headers: { Authorization: `Bearer ${accessToken}` }, - body: JSON.stringify({ - title, - body, - repoUrl: codemodRegistryRepoUrl, - }), - }, - ); - if (!result.ok) throw new Error('Network response was not ok.'); - const data = await result.json(); - if (result.status !== 200) { - await onFail(); - return { status: result.status, html_url: null }; - } - - const { data } = result; - - const validation = createIssueResponseCodec.decode(data); - - if (validation._tag === 'Left') { - await onFail(); - window.showErrorMessage('Creating Github issue failed.'); - return { status: 406, html_url: null }; - } - - onSuccess(); - - const decision = await window.showInformationMessage( - 'Github issue is successfully created.', - 'See issue in Github', - ); - const { html_url } = validation.right; - if (decision === 'See issue in Github') { - commands.executeCommand('codemod.redirect', html_url); - } - return { - status: 200, - html_url, - }; + // call API to create Github Issue + const codemodRegistryRepoUrl = "https://github.com/codemod-com/codemod"; + + const response = await extendedFetch( + "https://backend.codemod.com/sourceControl/github/issues", + { + method: "POST", + headers: { Authorization: `Bearer ${accessToken}` }, + body: JSON.stringify({ + title, + body, + repoUrl: codemodRegistryRepoUrl, + }), + }, + ); + if (response.status !== 200) { + await onFail(); + return { status: response.status, html_url: null }; + } + + const data = (await response.json()) as any; + + const validation = createIssueResponseCodec.decode(data); + + if (validation._tag === "Left") { + await onFail(); + window.showErrorMessage("Creating Github issue failed."); + return { status: 406, html_url: null }; + } + + onSuccess(); + + const decision = await window.showInformationMessage( + "Github issue is successfully created.", + "See issue in Github", + ); + const { html_url } = validation.right; + if (decision === "See issue in Github") { + commands.executeCommand("codemod.redirect", html_url); + } + return { + status: 200, + html_url, + }; }; export class MainViewProvider implements WebviewViewProvider { - private __view: WebviewView | null = null; - private __webviewResolver: WebviewResolver; - private __executionQueue: ReadonlyArray<CodemodHash> = []; - private __directoryPaths: ReadonlyArray<string> | null = null; - // true by default to prevent banner blinking on load - private __codemodEngineNodeLocated = true; - - constructor( - context: ExtensionContext, - private readonly __engineService: EngineService, - private readonly __messageBus: MessageBus, - private readonly __rootUri: Uri | null, - private readonly __store: Store, - ) { - this.__webviewResolver = new WebviewResolver(context.extensionUri); - - this.__messageBus.subscribe(MessageKind.showProgress, (message) => { - if (message.codemodHash === null) { - return; - } - - this.__postMessage({ - kind: 'webview.global.setCodemodExecutionProgress', - codemodHash: message.codemodHash, - progressKind: message.progressKind, - totalFileNumber: message.totalFileNumber, - processedFileNumber: message.processedFileNumber, - }); - }); - - this.__messageBus.subscribe(MessageKind.codemodSetExecuted, () => { - this.__postMessage({ - kind: 'webview.global.codemodExecutionHalted', - }); - }); - - this.__messageBus.subscribe(MessageKind.executeCodemodSet, () => { - this.__store.dispatch(actions.collapseResultsPanel(false)); - this.__store.dispatch(actions.collapseChangeExplorerPanel(false)); - }); - - this.__messageBus.subscribe( - MessageKind.executionQueueChange, - (message) => { - this.__executionQueue = message.queuedCodemodHashes; - const props = this.__buildProps(); - - this.__postMessage({ - kind: 'webview.main.setProps', - props: props, - }); - }, - ); - - this.__messageBus.subscribe( - MessageKind.codemodEngineNodeLocated, - ({ codemodEngineNodeLocated }) => { - if ( - this.__codemodEngineNodeLocated === codemodEngineNodeLocated - ) { - return; - } - - this.__codemodEngineNodeLocated = codemodEngineNodeLocated; - - const props = this.__buildProps(); - - this.__postMessage({ - kind: 'webview.main.setProps', - props, - }); - }, - ); - - let prevProps = this.__buildProps(); - - this.__store.subscribe(async () => { - if (this.__directoryPaths === null) { - await this.__getDirectoryPaths(); - } - - const nextProps = this.__buildProps(); - if (areEqual(prevProps, nextProps)) { - return; - } - - prevProps = nextProps; - - this.__postMessage({ - kind: 'webview.main.setProps', - props: nextProps, - }); - }); - } - - public isVisible(): boolean { - return this.__view?.visible ?? false; - } - - public resolveWebviewView(webviewView: WebviewView): void | Thenable<void> { - this.__resolveWebview(webviewView); - - this.__view = webviewView; - - this.__view.webview.onDidReceiveMessage(this.__onDidReceiveMessage); - - this.__messageBus.publish({ - kind: MessageKind.mainWebviewViewVisibilityChange, - }); - - this.__view.onDidChangeVisibility(() => { - this.__messageBus.publish({ - kind: MessageKind.mainWebviewViewVisibilityChange, - }); - - if (this.__view?.visible) { - this.__resolveWebview(this.__view); - } - }); - } - - private async __getDirectoryPaths() { - if (this.__rootUri === null) { - return; - } - - const globPattern = buildGlobPattern(this.__rootUri, '/**'); - - // From `glob` documentation: - // (Note: to match only directories, put a / at the end of the pattern.) - const directoryPaths = await glob(`${globPattern}/`, { - // ignore node_modules and files, match only directories - ignore: ['**/node_modules/**'], - follow: false, - maxDepth: 10, - }); - - const MAX_NUMBER_OF_DIRECTORIES = 10000; - - this.__directoryPaths = directoryPaths.slice( - 0, - MAX_NUMBER_OF_DIRECTORIES, - ); - } - - private __postMessage(message: WebviewMessage) { - this.__view?.webview.postMessage(message); - } - - private __resolveWebview(webviewView: WebviewView) { - this.__webviewResolver.resolveWebview( - webviewView.webview, - 'main', - JSON.stringify(this.__buildProps()), - 'mainWebviewViewProps', - ); - } - - private __buildProps() { - return selectMainWebviewViewProps( - this.__store.getState(), - this.__rootUri, - this.__directoryPaths, - this.__executionQueue, - this.__codemodEngineNodeLocated, - ); - } - - private __onDidReceiveMessage = async (message: WebviewResponse) => { - if (message.kind === 'webview.command') { - commands.executeCommand( - message.value.command, - ...(message.value.arguments ?? []), - ); - } - - if (message.kind === 'webview.campaignManager.setSelectedCaseHash') { - this.__store.dispatch( - actions.setSelectedCaseHash(message.caseHash), - ); - } - - if (message.kind === 'webview.global.discardSelected') { - commands.executeCommand( - 'codemod.discardJobs', - message.caseHashDigest, - ); - } - - if (message.kind === 'webview.global.showInformationMessage') { - window.showInformationMessage(message.value); - } - - if (message.kind === 'webview.global.applySelected') { - commands.executeCommand( - 'codemod.sourceControl.saveStagedJobsToTheFileSystem', - message.caseHashDigest, - ); - } - - if (message.kind === 'webview.main.setActiveTabId') { - this.__store.dispatch(actions.setActiveTabId(message.activeTabId)); - } - - if ( - message.kind === - 'webview.main.setCodemodDiscoveryPanelGroupSettings' - ) { - this.__store.dispatch( - actions.setCodemodDiscoveryPanelGroupSettings( - message.panelGroupSettings, - ), - ); - } - - if (message.kind === 'webview.main.setCodemodRunsPanelGroupSettings') { - this.__store.dispatch( - actions.setCodemodRunsPanelGroupSettings( - message.panelGroupSettings, - ), - ); - } - - if (message.kind === 'webview.main.setToaster') { - this.__store.dispatch(actions.setToaster(message.value)); - } - - if (message.kind === 'webview.global.flipSelectedExplorerNode') { - this.__store.dispatch( - actions.flipSelectedExplorerNode([ - message.caseHashDigest, - message.explorerNodeHashDigest, - ]), - ); - } - - if (message.kind === 'webview.global.flipCollapsibleExplorerNode') { - this.__store.dispatch( - actions.flipCollapsibleExplorerNode([ - message.caseHashDigest, - message.explorerNodeHashDigest, - ]), - ); - } - - if (message.kind === 'webview.global.focusExplorerNode') { - this.__store.dispatch( - actions.focusExplorerNode([ - message.caseHashDigest, - message.explorerNodeHashDigest, - ]), - ); - } - - if (message.kind === 'webview.global.setChangeExplorerSearchPhrase') { - this.__store.dispatch( - actions.setChangeExplorerSearchPhrase([ - message.caseHashDigest, - message.searchPhrase, - ]), - ); - } - - if (message.kind === 'webview.codemodList.haltCodemodExecution') { - this.__engineService.shutdownEngines(); - } - - if (message.kind === 'webview.codemodList.dryRunCodemod') { - if (this.__rootUri === null) { - window.showWarningMessage('No active workspace is found.'); - return; - } - - const hashDigest = message.value; - this.__store.dispatch(actions.setRecentCodemodHashes(hashDigest)); - - const state = this.__store.getState().codemodDiscoveryView; - const executionPath = - state.executionPaths[hashDigest] ?? this.__rootUri.fsPath; - - if (executionPath === null) { - return; - } - - const uri = Uri.file(executionPath); - - // if missing some required arguments, open arguments popup - - const argumentsSpecified = selectCodemodArguments( - this.__store.getState(), - hashDigest as unknown as CodemodNodeHashDigest, - ).every( - ({ required, value }) => - !required || - (isNeitherNullNorUndefined(value) && value !== ''), - ); - - if (!argumentsSpecified) { - this.__store.dispatch( - actions.setCodemodArgumentsPopupHashDigest( - hashDigest as unknown as CodemodNodeHashDigest, - ), - ); - return; - } - - commands.executeCommand('codemod.executeCodemod', uri, hashDigest); - } - - if (message.kind === 'webview.codemodList.updatePathToExecute') { - await this.updateExecutionPath(message.value); - - this.__postMessage({ - kind: 'webview.main.setProps', - props: this.__buildProps(), - }); - } - - if (message.kind === 'webview.global.showWarningMessage') { - window.showWarningMessage(message.value); - } - - if (message.kind === 'webview.global.flipCodemodHashDigest') { - this.__store.dispatch( - actions.flipCodemodHashDigest(message.codemodNodeHashDigest), - ); - } - - if (message.kind === 'webview.global.selectCodemodNodeHashDigest') { - this.__store.dispatch( - actions.setFocusedCodemodHashDigest( - message.selectedCodemodNodeHashDigest, - ), - ); - } - - if (message.kind === 'webview.global.setCodemodSearchPhrase') { - this.__store.dispatch( - actions.setCodemodSearchPhrase(message.searchPhrase), - ); - } - - if (message.kind === 'webview.global.collapseResultsPanel') { - this.__store.dispatch( - actions.collapseResultsPanel(message.collapsed), - ); - } - - if (message.kind === 'webview.global.collapseChangeExplorerPanel') { - this.__store.dispatch( - actions.collapseChangeExplorerPanel(message.collapsed), - ); - } - - if ( - message.kind === 'webview.global.setCodemodArgumentsPopupHashDigest' - ) { - this.__store.dispatch( - actions.setCodemodArgumentsPopupHashDigest(message.hashDigest), - ); - } - - if (message.kind === 'webview.global.setCodemodArgument') { - this.__store.dispatch( - actions.setCodemodArgument({ - hashDigest: message.hashDigest, - name: message.name, - value: message.value, - }), - ); - } - }; - - public updateExecutionPath = async ({ - newPath, - codemodHash, - errorMessage, - warningMessage, - revertToPrevExecutionIfInvalid, - fromVSCodeCommand, - }: { - newPath: string; - codemodHash: CodemodHash; - errorMessage: string | null; - warningMessage: string | null; - revertToPrevExecutionIfInvalid: boolean; - fromVSCodeCommand?: boolean; - }) => { - if (this.__rootUri === null) { - window.showWarningMessage('No active workspace is found.'); - return; - } - - const state = this.__store.getState().codemodDiscoveryView; - const persistedExecutionPath = state.executionPaths[codemodHash]; - - const oldExecutionPath = persistedExecutionPath ?? null; - const newPathAbsolute = relativeToAbsolutePath( - newPath, - this.__rootUri.fsPath, - ); - - try { - await workspace.fs.stat(Uri.file(newPathAbsolute)); - this.__store.dispatch( - actions.setExecutionPath({ - codemodHash, - path: newPathAbsolute, - }), - ); - - if (!fromVSCodeCommand) { - window.showInformationMessage( - 'Successfully updated the execution path.', - ); - } - } catch (e) { - if (errorMessage !== null) { - window.showErrorMessage(errorMessage); - } - if (warningMessage !== null) { - window.showWarningMessage(warningMessage); - } - - if (oldExecutionPath === null) { - return; - } - - if (revertToPrevExecutionIfInvalid) { - this.__store.dispatch( - actions.setExecutionPath({ - codemodHash, - path: oldExecutionPath, - }), - ); - } else { - this.__store.dispatch( - actions.setExecutionPath({ - codemodHash, - path: oldExecutionPath, - }), - ); - } - } - }; + private __view: WebviewView | null = null; + private __webviewResolver: WebviewResolver; + private __executionQueue: ReadonlyArray<CodemodHash> = []; + private __directoryPaths: ReadonlyArray<string> | null = null; + // true by default to prevent banner blinking on load + private __codemodEngineNodeLocated = true; + + constructor( + context: ExtensionContext, + private readonly __engineService: EngineService, + private readonly __messageBus: MessageBus, + private readonly __rootUri: Uri | null, + private readonly __store: Store, + ) { + this.__webviewResolver = new WebviewResolver(context.extensionUri); + + this.__messageBus.subscribe(MessageKind.showProgress, (message) => { + if (message.codemodHash === null) { + return; + } + + this.__postMessage({ + kind: "webview.global.setCodemodExecutionProgress", + codemodHash: message.codemodHash, + progressKind: message.progressKind, + totalFileNumber: message.totalFileNumber, + processedFileNumber: message.processedFileNumber, + }); + }); + + this.__messageBus.subscribe(MessageKind.codemodSetExecuted, () => { + this.__postMessage({ + kind: "webview.global.codemodExecutionHalted", + }); + }); + + this.__messageBus.subscribe(MessageKind.executeCodemodSet, () => { + this.__store.dispatch(actions.collapseResultsPanel(false)); + this.__store.dispatch(actions.collapseChangeExplorerPanel(false)); + }); + + this.__messageBus.subscribe(MessageKind.executionQueueChange, (message) => { + this.__executionQueue = message.queuedCodemodHashes; + const props = this.__buildProps(); + + this.__postMessage({ + kind: "webview.main.setProps", + props: props, + }); + }); + + this.__messageBus.subscribe( + MessageKind.codemodEngineNodeLocated, + ({ codemodEngineNodeLocated }) => { + if (this.__codemodEngineNodeLocated === codemodEngineNodeLocated) { + return; + } + + this.__codemodEngineNodeLocated = codemodEngineNodeLocated; + + const props = this.__buildProps(); + + this.__postMessage({ + kind: "webview.main.setProps", + props, + }); + }, + ); + + let prevProps = this.__buildProps(); + + this.__store.subscribe(async () => { + if (this.__directoryPaths === null) { + await this.__getDirectoryPaths(); + } + + const nextProps = this.__buildProps(); + if (areEqual(prevProps, nextProps)) { + return; + } + + prevProps = nextProps; + + this.__postMessage({ + kind: "webview.main.setProps", + props: nextProps, + }); + }); + } + + public isVisible(): boolean { + return this.__view?.visible ?? false; + } + + public resolveWebviewView(webviewView: WebviewView): void | Thenable<void> { + this.__resolveWebview(webviewView); + + this.__view = webviewView; + + this.__view.webview.onDidReceiveMessage(this.__onDidReceiveMessage); + + this.__messageBus.publish({ + kind: MessageKind.mainWebviewViewVisibilityChange, + }); + + this.__view.onDidChangeVisibility(() => { + this.__messageBus.publish({ + kind: MessageKind.mainWebviewViewVisibilityChange, + }); + + if (this.__view?.visible) { + this.__resolveWebview(this.__view); + } + }); + } + + private async __getDirectoryPaths() { + if (this.__rootUri === null) { + return; + } + + const globPattern = buildGlobPattern(this.__rootUri, "/**"); + + // From `glob` documentation: + // (Note: to match only directories, put a / at the end of the pattern.) + const directoryPaths = await glob(`${globPattern}/`, { + // ignore node_modules and files, match only directories + ignore: ["**/node_modules/**"], + follow: false, + maxDepth: 10, + }); + + const MAX_NUMBER_OF_DIRECTORIES = 10000; + + this.__directoryPaths = directoryPaths.slice(0, MAX_NUMBER_OF_DIRECTORIES); + } + + private __postMessage(message: WebviewMessage) { + this.__view?.webview.postMessage(message); + } + + private __resolveWebview(webviewView: WebviewView) { + this.__webviewResolver.resolveWebview( + webviewView.webview, + "main", + JSON.stringify(this.__buildProps()), + "mainWebviewViewProps", + ); + } + + private __buildProps() { + return selectMainWebviewViewProps( + this.__store.getState(), + this.__rootUri, + this.__directoryPaths, + this.__executionQueue, + this.__codemodEngineNodeLocated, + ); + } + + private __onDidReceiveMessage = async (message: WebviewResponse) => { + if (message.kind === "webview.command") { + commands.executeCommand( + message.value.command, + ...(message.value.arguments ?? []), + ); + } + + if (message.kind === "webview.campaignManager.setSelectedCaseHash") { + this.__store.dispatch(actions.setSelectedCaseHash(message.caseHash)); + } + + if (message.kind === "webview.global.discardSelected") { + commands.executeCommand("codemod.discardJobs", message.caseHashDigest); + } + + if (message.kind === "webview.global.showInformationMessage") { + window.showInformationMessage(message.value); + } + + if (message.kind === "webview.global.applySelected") { + commands.executeCommand( + "codemod.sourceControl.saveStagedJobsToTheFileSystem", + message.caseHashDigest, + ); + } + + if (message.kind === "webview.main.setActiveTabId") { + this.__store.dispatch(actions.setActiveTabId(message.activeTabId)); + } + + if (message.kind === "webview.main.setCodemodDiscoveryPanelGroupSettings") { + this.__store.dispatch( + actions.setCodemodDiscoveryPanelGroupSettings( + message.panelGroupSettings, + ), + ); + } + + if (message.kind === "webview.main.setCodemodRunsPanelGroupSettings") { + this.__store.dispatch( + actions.setCodemodRunsPanelGroupSettings(message.panelGroupSettings), + ); + } + + if (message.kind === "webview.main.setToaster") { + this.__store.dispatch(actions.setToaster(message.value)); + } + + if (message.kind === "webview.global.flipSelectedExplorerNode") { + this.__store.dispatch( + actions.flipSelectedExplorerNode([ + message.caseHashDigest, + message.explorerNodeHashDigest, + ]), + ); + } + + if (message.kind === "webview.global.flipCollapsibleExplorerNode") { + this.__store.dispatch( + actions.flipCollapsibleExplorerNode([ + message.caseHashDigest, + message.explorerNodeHashDigest, + ]), + ); + } + + if (message.kind === "webview.global.focusExplorerNode") { + this.__store.dispatch( + actions.focusExplorerNode([ + message.caseHashDigest, + message.explorerNodeHashDigest, + ]), + ); + } + + if (message.kind === "webview.global.setChangeExplorerSearchPhrase") { + this.__store.dispatch( + actions.setChangeExplorerSearchPhrase([ + message.caseHashDigest, + message.searchPhrase, + ]), + ); + } + + if (message.kind === "webview.codemodList.haltCodemodExecution") { + this.__engineService.shutdownEngines(); + } + + if (message.kind === "webview.codemodList.dryRunCodemod") { + if (this.__rootUri === null) { + window.showWarningMessage("No active workspace is found."); + return; + } + + const hashDigest = message.value; + this.__store.dispatch(actions.setRecentCodemodHashes(hashDigest)); + + const state = this.__store.getState().codemodDiscoveryView; + const executionPath = + state.executionPaths[hashDigest] ?? this.__rootUri.fsPath; + + if (executionPath === null) { + return; + } + + const uri = Uri.file(executionPath); + + // if missing some required arguments, open arguments popup + + const argumentsSpecified = selectCodemodArguments( + this.__store.getState(), + hashDigest as unknown as CodemodNodeHashDigest, + ).every( + ({ required, value }) => + !required || (isNeitherNullNorUndefined(value) && value !== ""), + ); + + if (!argumentsSpecified) { + this.__store.dispatch( + actions.setCodemodArgumentsPopupHashDigest( + hashDigest as unknown as CodemodNodeHashDigest, + ), + ); + return; + } + + commands.executeCommand("codemod.executeCodemod", uri, hashDigest); + } + + if (message.kind === "webview.codemodList.updatePathToExecute") { + await this.updateExecutionPath(message.value); + + this.__postMessage({ + kind: "webview.main.setProps", + props: this.__buildProps(), + }); + } + + if (message.kind === "webview.global.showWarningMessage") { + window.showWarningMessage(message.value); + } + + if (message.kind === "webview.global.flipCodemodHashDigest") { + this.__store.dispatch( + actions.flipCodemodHashDigest(message.codemodNodeHashDigest), + ); + } + + if (message.kind === "webview.global.selectCodemodNodeHashDigest") { + this.__store.dispatch( + actions.setFocusedCodemodHashDigest( + message.selectedCodemodNodeHashDigest, + ), + ); + } + + if (message.kind === "webview.global.setCodemodSearchPhrase") { + this.__store.dispatch( + actions.setCodemodSearchPhrase(message.searchPhrase), + ); + } + + if (message.kind === "webview.global.collapseResultsPanel") { + this.__store.dispatch(actions.collapseResultsPanel(message.collapsed)); + } + + if (message.kind === "webview.global.collapseChangeExplorerPanel") { + this.__store.dispatch( + actions.collapseChangeExplorerPanel(message.collapsed), + ); + } + + if (message.kind === "webview.global.setCodemodArgumentsPopupHashDigest") { + this.__store.dispatch( + actions.setCodemodArgumentsPopupHashDigest(message.hashDigest), + ); + } + + if (message.kind === "webview.global.setCodemodArgument") { + this.__store.dispatch( + actions.setCodemodArgument({ + hashDigest: message.hashDigest, + name: message.name, + value: message.value, + }), + ); + } + }; + + public updateExecutionPath = async ({ + newPath, + codemodHash, + errorMessage, + warningMessage, + revertToPrevExecutionIfInvalid, + fromVSCodeCommand, + }: { + newPath: string; + codemodHash: CodemodHash; + errorMessage: string | null; + warningMessage: string | null; + revertToPrevExecutionIfInvalid: boolean; + fromVSCodeCommand?: boolean; + }) => { + if (this.__rootUri === null) { + window.showWarningMessage("No active workspace is found."); + return; + } + + const state = this.__store.getState().codemodDiscoveryView; + const persistedExecutionPath = state.executionPaths[codemodHash]; + + const oldExecutionPath = persistedExecutionPath ?? null; + const newPathAbsolute = relativeToAbsolutePath( + newPath, + this.__rootUri.fsPath, + ); + + try { + await workspace.fs.stat(Uri.file(newPathAbsolute)); + this.__store.dispatch( + actions.setExecutionPath({ + codemodHash, + path: newPathAbsolute, + }), + ); + + if (!fromVSCodeCommand) { + window.showInformationMessage( + "Successfully updated the execution path.", + ); + } + } catch (e) { + if (errorMessage !== null) { + window.showErrorMessage(errorMessage); + } + if (warningMessage !== null) { + window.showWarningMessage(warningMessage); + } + + if (oldExecutionPath === null) { + return; + } + + if (revertToPrevExecutionIfInvalid) { + this.__store.dispatch( + actions.setExecutionPath({ + codemodHash, + path: oldExecutionPath, + }), + ); + } else { + this.__store.dispatch( + actions.setExecutionPath({ + codemodHash, + path: oldExecutionPath, + }), + ); + } + } + }; } diff --git a/apps/vsce/src/fetch/index.ts b/apps/vsce/src/fetch/index.ts new file mode 100644 index 000000000..4c6cff743 --- /dev/null +++ b/apps/vsce/src/fetch/index.ts @@ -0,0 +1,23 @@ +import { FetchError, extendedFetch } from "@codemod-com/utilities"; + +export const retryingClient = async ( + url: string, + options?: RequestInit & { retries?: number }, +) => { + let retryCount = options?.retries ?? DEFAULT_RETRY_COUNT; + while (retryCount > 0) { + try { + const response = await extendedFetch(url, options); + return response; + } catch (err) { + retryCount -= 1; + if (retryCount === 0) { + throw err; + } + } + } + + throw new FetchError("Failed to fetch"); +}; + +export const DEFAULT_RETRY_COUNT = 5; diff --git a/apps/vsce/test/dowloadService.test.ts b/apps/vsce/test/dowloadService.test.ts index a66a6ab11..9f74ff70c 100644 --- a/apps/vsce/test/dowloadService.test.ts +++ b/apps/vsce/test/dowloadService.test.ts @@ -1,8 +1,7 @@ -import { AxiosError, type AxiosInstance } from "axios"; +import { FetchError } from "@codemod-com/utilities"; import nock from "nock"; import { afterEach, describe, expect, test, vi } from "vitest"; import type { FileSystem } from "vscode"; -import { retryingClient as axiosInstance } from "../src/axios"; import { DownloadService } from "../src/components/downloadService"; const mockedFileSystemUtilities = { @@ -21,7 +20,7 @@ const downloadService = new DownloadService( mockedFileSystemUtilities, ); -const NETWORK_ERROR = new AxiosError("Some connection error"); +const NETWORK_ERROR = new FetchError("Some connection error"); NETWORK_ERROR.code = "ECONNRESET"; // 3 failed responses, then good response @@ -39,28 +38,16 @@ const responses = [ () => nock("https://test.com").get("/test").reply(200, "Test"), ]; -const setupResponses = ( - client: AxiosInstance, - responses: Array<() => void>, -) => { - const configureResponse = () => { +const originalFetch = global.fetch; +global.fetch = vi + .fn() + .mockImplementation((url: string, options?: RequestInit) => { const response = responses.shift(); if (response) { response(); } - }; - - client.interceptors.request.use( - (config) => { - configureResponse(); - return config; - }, - (error) => { - configureResponse(); - return Promise.reject(error); - }, - ); -}; + return originalFetch(url, options); + }); describe("DownloadService", () => { afterEach(() => { @@ -69,8 +56,6 @@ describe("DownloadService", () => { }); test("Should retry 3 times if request fails", async () => { - setupResponses(axiosInstance, responses); - await downloadService.downloadFileIfNeeded( "https://test.com/test", // @ts-expect-error passing a string instead of URI, because URI cannot be imported from vscode diff --git a/apps/vsce/tsconfig.json b/apps/vsce/tsconfig.json index db7635554..740ec38b1 100644 --- a/apps/vsce/tsconfig.json +++ b/apps/vsce/tsconfig.json @@ -1,5 +1,5 @@ { "include": ["./src/**/*.ts", "./src/types/**/*.d.ts", "./test/**/*.ts"], "extends": "@codemod-com/tsconfig/extension.json", - "compilerOptions": { "lib": ["ES2021"] } + "compilerOptions": { "lib": ["ES2021", "DOM"] } } diff --git a/packages/codemods/axios/fetch/src/index.ts b/packages/codemods/axios/fetch/src/index.ts index 9e5c42958..1428c58a5 100644 --- a/packages/codemods/axios/fetch/src/index.ts +++ b/packages/codemods/axios/fetch/src/index.ts @@ -25,6 +25,10 @@ export async function workflow({ files }: Api) { { pattern: "axios.$_($$$).$_($$$)" }, // axios.get(...).then(...) { pattern: "axios.$_($$$).$_($$$).$_($$$)" }, // axios.get(...).then(...).catch(...) { pattern: "axios.$_($$$).$_($$$).$_($$$).$_($$$)" }, // axios.get(...).then(...).catch(...).finally(...) + { pattern: "axios.$_<$_>($$$)" }, // axios.get(...) + { pattern: "axios.$_<$_>($$$).$_($$$)" }, // axios.get(...).then(...) + { pattern: "axios.$_<$_>($$$).$_($$$).$_($$$)" }, // axios.get(...).then(...).catch(...) + { pattern: "axios.$_<$_>($$$).$_($$$).$_($$$).$_($$$)" }, // axios.get(...).then(...).catch(...).finally(...) ]; const extendAxiosPatterns = (extend: (pattern: string) => string) => diff --git a/packages/runner/package.json b/packages/runner/package.json index 4cd76e07b..f5eff32cf 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -32,8 +32,8 @@ "vitest": "^1.0.1" }, "dependencies": { - "@ast-grep/cli": "^0.22.3", - "@ast-grep/napi": "^0.22.3", + "@ast-grep/cli": "^0.24.0", + "@ast-grep/napi": "^0.24.0", "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "@babel/preset-env": "^7.24.1", diff --git a/packages/utilities/src/fetch.ts b/packages/utilities/src/fetch.ts new file mode 100644 index 000000000..1f340fd55 --- /dev/null +++ b/packages/utilities/src/fetch.ts @@ -0,0 +1,45 @@ +const globalHooks: ((options: RequestInit) => RequestInit)[] = []; + +export class FetchError extends Error { + public code?: string; + + constructor( + message: string, + public response?: Response, + ) { + super(message); + } +} + +export function isFetchError(error: unknown): error is FetchError { + return error instanceof FetchError; +} + +export const addGlobalHook = (hook: (options: RequestInit) => RequestInit) => { + globalHooks.push(hook); +}; + +export const extendedFetch = async ( + url: string, + initialOptions: RequestInit = {}, +) => { + let options = initialOptions; + try { + for (const hook of globalHooks) { + options = hook(options); + } + const response = await fetch(url, options); + + if (!response.ok) { + throw new FetchError("Failed to fetch", response); + } + + return response; + } catch (e) { + if (isFetchError(e)) { + throw e; + } + + throw new FetchError("Failed to fetch"); + } +}; diff --git a/packages/utilities/src/index.ts b/packages/utilities/src/index.ts index d97a6f3ed..d37dcaa22 100644 --- a/packages/utilities/src/index.ts +++ b/packages/utilities/src/index.ts @@ -107,3 +107,4 @@ export { CaseReadingService } from "./services/case/caseReadingService.js"; export { CaseWritingService } from "./services/case/caseWritingService.js"; export { FileWatcher } from "./services/case/fileWatcher.js"; export { TarService } from "./services/tar.js"; +export * from "./fetch.js"; diff --git a/packages/utilities/tsconfig.json b/packages/utilities/tsconfig.json index 19d757df2..ec62541a4 100644 --- a/packages/utilities/tsconfig.json +++ b/packages/utilities/tsconfig.json @@ -4,6 +4,7 @@ "compilerOptions": { "module": "NodeNext", "moduleResolution": "nodenext", + "lib": ["ES2021", "DOM"], "types": ["node"], "baseUrl": ".", "target": "es2021", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index a07373d57..d205e2e47 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -30,8 +30,8 @@ "directory": "packages/workflow" }, "dependencies": { - "@ast-grep/cli": "^0.22.3", - "@ast-grep/napi": "^0.22.3", + "@ast-grep/cli": "^0.24.0", + "@ast-grep/napi": "^0.24.0", "@sindresorhus/slugify": "^2.2.1", "colors-cli": "^1.0.33", "filenamify": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 334c8a7f9..743dfd8e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,9 +75,6 @@ importers: '@fastify/rate-limit': specifier: 9.0.1 version: 9.0.1 - axios: - specifier: ^1.6.8 - version: 1.6.8 dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -148,9 +145,6 @@ importers: ai: specifier: 2.2.29 version: 2.2.29(react@18.2.0)(solid-js@1.8.17)(svelte@4.2.15)(vue@3.4.25(typescript@5.3.3)) - axios: - specifier: ^1.6.8 - version: 1.6.8 bullmq: specifier: ^5.7.5 version: 5.7.5 @@ -267,11 +261,11 @@ importers: apps/cli: dependencies: '@ast-grep/cli': - specifier: ^0.22.3 - version: 0.22.4 + specifier: ^0.24.0 + version: 0.24.0 '@ast-grep/napi': - specifier: ^0.22.3 - version: 0.22.4 + specifier: ^0.24.0 + version: 0.24.0 esbuild: specifier: ^0.17.14 version: 0.17.19 @@ -321,9 +315,6 @@ importers: '@vitest/coverage-v8': specifier: ^1.0.1 version: 1.5.1(vitest@1.5.1(@types/node@18.11.9)(jsdom@23.2.0)(terser@5.30.4)) - axios: - specifier: ^1.6.8 - version: 1.6.8 columnify: specifier: ^1.6.0 version: 1.6.0 @@ -513,9 +504,6 @@ importers: ast-types: specifier: ^0.14.2 version: 0.14.2 - axios: - specifier: ^1.6.8 - version: 1.6.8 change-case: specifier: ^5.2.0 version: 5.4.4 @@ -838,9 +826,6 @@ importers: ai: specifier: 2.2.29 version: 2.2.29(react@18.2.0)(solid-js@1.8.17)(svelte@4.2.15)(vue@3.4.25(typescript@4.9.5)) - axios: - specifier: ^1.6.8 - version: 1.6.8 chatgpt: specifier: 5.2.5 version: 5.2.5 @@ -884,9 +869,9 @@ importers: apps/task-manager: dependencies: - axios: - specifier: ^1.6.8 - version: 1.6.8 + '@codemod-com/utilities': + specifier: workspace:* + version: link:../../packages/utilities bullmq: specifier: ^5.7.2 version: 5.7.5 @@ -942,12 +927,6 @@ importers: '@vscode/vsce': specifier: ^2.22.0 version: 2.26.0 - axios: - specifier: ^1.6.8 - version: 1.6.8 - axios-retry: - specifier: ^4.0.0 - version: 4.1.0(axios@1.6.8) diff: specifier: ^5.1.0 version: 5.2.0 @@ -979,8 +958,8 @@ importers: specifier: ^0.3.5 version: 0.3.5(fp-ts@2.16.5)(monocle-ts@2.3.13(fp-ts@2.16.5)) nock: - specifier: ^13.5.1 - version: 13.5.4 + specifier: beta + version: 14.0.0-beta.7 redux-persist: specifier: ^6.0.0 version: 6.0.0(react@18.2.0)(redux@4.2.1) @@ -5032,11 +5011,11 @@ importers: packages/runner: dependencies: '@ast-grep/cli': - specifier: ^0.22.3 - version: 0.22.4 + specifier: ^0.24.0 + version: 0.24.0 '@ast-grep/napi': - specifier: ^0.22.3 - version: 0.22.4 + specifier: ^0.24.0 + version: 0.24.0 '@babel/core': specifier: ^7.24.4 version: 7.24.4 @@ -5253,11 +5232,11 @@ importers: packages/workflow: dependencies: '@ast-grep/cli': - specifier: ^0.22.3 - version: 0.22.4 + specifier: ^0.24.0 + version: 0.24.0 '@ast-grep/napi': - specifier: ^0.22.3 - version: 0.22.4 + specifier: ^0.24.0 + version: 0.24.0 '@sindresorhus/slugify': specifier: ^2.2.1 version: 2.2.1 @@ -5352,99 +5331,198 @@ packages: cpu: [arm64] os: [darwin] + '@ast-grep/cli-darwin-arm64@0.24.0': + resolution: {integrity: sha512-8YkcaYNzy970snexNS2+8O+Uk8wrmgUELFdoVcJMIdzeTppbzKbAypJkTrOxosCIOoO6Bq0vp8hvmZj04wXhlQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@ast-grep/cli-darwin-x64@0.22.4': resolution: {integrity: sha512-gKUznVB4WgNrKCSco1J1cJ7g9eeMTktFGFDVynH9DUECB/ZFSyIJ82SIkvfMBYpO5ToR0tK2p9O8Ze4biWkI5Q==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] + '@ast-grep/cli-darwin-x64@0.24.0': + resolution: {integrity: sha512-U7S2uZQmT4B79cVfvwd785yFj7ZF02hjhBhAuVQq81LPI8mE8NH/phmjoSoL1u1Olf3BxqUcBLFdM4zoLvryiw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@ast-grep/cli-linux-arm64-gnu@0.22.4': resolution: {integrity: sha512-C6s+Ztx+IqKarvnykm/IfIsKW8VUA4g5EpJrFKul8C+qrwJm+35FRVDr4pKZ3R9ioB0P6G5XxMA04H1bYdTGjQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + '@ast-grep/cli-linux-arm64-gnu@0.24.0': + resolution: {integrity: sha512-NVn83DrcbBhrKwO3yNDdEPv05lr3TDq0pfDg8eEBfJQnZnvXliEYxJwyyzZ53867rLeqaPo4jvPHcIJKdKe0rw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@ast-grep/cli-linux-x64-gnu@0.22.4': resolution: {integrity: sha512-ZcFvXr+yFHqXULdb9tNr8kjQwx+rA35UCq753sQxJ94VJ+jPvMLCzLOooFaEWOjZKiV3RNHjwQFIOd/O92yHcA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + '@ast-grep/cli-linux-x64-gnu@0.24.0': + resolution: {integrity: sha512-OMVq58p2BsMVBoh/cdSDmmUiIA+78FAyZXYWZ8mS1BudU6Ba+15ZVqeId90Q4U9X+yEqIHyaz+X1CcLiygnQxg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@ast-grep/cli-win32-arm64-msvc@0.22.4': resolution: {integrity: sha512-uLlfPP/cNy9DFdQHLdc61xwBBqRK3PIk066kxJYH27BE1FleHZMiQEO9o4of6dgstYDeoIN5EMgSpinYYdeMCA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] + '@ast-grep/cli-win32-arm64-msvc@0.24.0': + resolution: {integrity: sha512-02CE3PfvwEsKvTITBP+J3Dm87MCeZl+t0Q4vBhQhcMBQt7N7D9DPY5tm0mVcgioY0Fm1yGn2z7IG56Dd6Pmnmw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@ast-grep/cli-win32-ia32-msvc@0.22.4': resolution: {integrity: sha512-0Ct+4iUf1yj2gtSC6dF0u4lszgak8mGs7aHpVhXGsygO3A29vJPX8P4zCP9AjKDJFUtxHb5sJG0HCoc/4XbUmA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] + '@ast-grep/cli-win32-ia32-msvc@0.24.0': + resolution: {integrity: sha512-JB0dFaPTf8JSuKsID4vV/FYGx9kDTxwvzSeNYXebmoEgiS7catiRWBmB4LSuJMvdd5RCSQskDRu733Qsyd4CoA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + '@ast-grep/cli-win32-x64-msvc@0.22.4': resolution: {integrity: sha512-VHADVsljesHLB3MTPCE4a6UYZHkwzZSTbavRG8X4xdXWpaQMf4h7FyF5P/JkXB3zSJWkYs6oTtVSy78pcJ4IAQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] + '@ast-grep/cli-win32-x64-msvc@0.24.0': + resolution: {integrity: sha512-73LDMKe/O+wg1dAVww104UsAHo7u4GBcC+qzHODqGGP1j3QtESyDYRrZfiJAHhkTRSjitVZrxJ7SZzRmbaJn4g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@ast-grep/cli@0.22.4': resolution: {integrity: sha512-SsV8gP8gdPjsb2ov9T0uZ+pmBllLXpsSgBuQpO8MjYHNJBoCzpK/lmrjFj+Fs9NeJvvRuZJrhu1GfhpX+bumpw==} engines: {node: '>= 12.0.0'} hasBin: true + '@ast-grep/cli@0.24.0': + resolution: {integrity: sha512-sK1/Ozd2HWPOqft+nZGNamO6hCBpdHbmZtiatdsO1Yhz3VRt7m9LSiKNZ7dUfFw9PXn0LRqyZk9SoHq222KcHw==} + engines: {node: '>= 12.0.0'} + hasBin: true + '@ast-grep/napi-darwin-arm64@0.22.4': resolution: {integrity: sha512-q0F3tIqQh+zxNUTFr56vkxwh+f88rlnWyJYA1EjrtkQ6j4Yw/KgdMW3WfIxVEdglAJ3dcRg2w4uOXHTwnmfQIg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] + '@ast-grep/napi-darwin-arm64@0.24.0': + resolution: {integrity: sha512-5cj+N60E9019uHsMTcN9vKA/fghNiqN77Sd0XKBg35hDJMc54R5n+kpZ8t3YvP5sN2Rhtf9UbIhznXhB8DPSug==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@ast-grep/napi-darwin-x64@0.22.4': resolution: {integrity: sha512-Ci5IQdlXua+XXQ7qjv5210Hm9lU2w5d7heYS09sOpB/Wnd1KhdZVbSSOnx9pnVbp4CDv3ADQy7FM6HBJGxoyXg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] + '@ast-grep/napi-darwin-x64@0.24.0': + resolution: {integrity: sha512-AaZAFkjnQAZ9LxK6uOXRlJVZAQriO37+wyjPjLC7KSuxzcSUBtrV2mGNkHRfNO6GcVCLbNZkk6qOhFNrRVOIxw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@ast-grep/napi-linux-arm64-gnu@0.22.4': resolution: {integrity: sha512-5U6iOrbIOY/MpGcQBJSS418kLZtqkTMY51EH0H+aRh1MB5mKgcpyUuIJnFxI22uvjI/ZUkgp7Mh+S+Y+OgvD/g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + '@ast-grep/napi-linux-arm64-gnu@0.24.0': + resolution: {integrity: sha512-/CEVtWpeHZrFeoc5GBVZ0E70FULls1EKHze8r7stVTSkOhIsrFVOImxKlt98tXTM/zfS9BijVz41bHnTif2GUw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + '@ast-grep/napi-linux-x64-gnu@0.22.4': resolution: {integrity: sha512-xG3OnyCQ4WTj1z2ir8dBZPUe0IvAexEcNwc2s81wxz1g2MB0TUt94VQF25wXtmgCm9v/3lNA/422nkALus88pA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + '@ast-grep/napi-linux-x64-gnu@0.24.0': + resolution: {integrity: sha512-CyNMKz2yPb65uEIGwmBcBexweHdT7cyqYFVahTx4rZs+tHpnGVEKHwffQ1Kob+HCeeGZBUupDNXZqpWjp8q6Fw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@ast-grep/napi-linux-x64-musl@0.22.4': resolution: {integrity: sha512-f+/cUaRjxpZdqefe91ud+YYOLeIl6MwISXh7ao1ywA+7/qtlm1ds/dMHZtNvcl3cw9ujmI+enp8wIOd4nUakhw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + '@ast-grep/napi-linux-x64-musl@0.24.0': + resolution: {integrity: sha512-H3F7QG45Y9uXb3Kebe7RHDvFvbArNcBNG4qEpxq9T5eAyBWzbunMGC1oAZo6BgBJtO0Yhl3QyC2OvnuAB+4GWw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + '@ast-grep/napi-win32-arm64-msvc@0.22.4': resolution: {integrity: sha512-+ch/Z3/FJ67mbv2A1T8layLoH2NF/Lxv2Z9u4bdqQHqz/dXZTkYMbry7yIcRccbRsYlWbfVO8iOGDF+RdeiTAw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] + '@ast-grep/napi-win32-arm64-msvc@0.24.0': + resolution: {integrity: sha512-ZX0JQk7uZV+yaXVhn/mFSSf55JglYq0uAiXB53aAQCOyiKyaewl+tK4sPlcF1BpSh7D+SpP9GNIU4MAE8zB1aA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@ast-grep/napi-win32-ia32-msvc@0.22.4': resolution: {integrity: sha512-q1ElrjsPfrhItHnwolC16oTDjsh1tX2xirVVIAwmbK4+Wini3/6HDRpxOOIHr9QmkfGioabEJATZl7uQJiziUQ==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] + '@ast-grep/napi-win32-ia32-msvc@0.24.0': + resolution: {integrity: sha512-rCOEY27Wu3K/lqHXy1Wg7HoScJuHvsRdmlmfSw19HSv4vR2MQVQx/wd0P3WKzqS8LeDr333kQKbJO57dgDYg5Q==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + '@ast-grep/napi-win32-x64-msvc@0.22.4': resolution: {integrity: sha512-Xk/0ZWA422+9osSCkdOTgHnub2UtuUNHMgPtsGDLslHzp0PhozXi2NV6st5OBEgOkwIeSyxoTtLTVZ56+lL1rw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] + '@ast-grep/napi-win32-x64-msvc@0.24.0': + resolution: {integrity: sha512-049I7DvvC8IFpezOtda0Dj7C5V+Erl1Jhcsq14mUnUEe++nMldicss7D0CXZHVQ9UjPJ5zpxnqYxx05caMMDzA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@ast-grep/napi@0.22.4': resolution: {integrity: sha512-2kBrpVLGivW316WDb25ulau2nWYlyp+N+jLsVdoO3F2Ik80y6uilAbV80gw9SZwlnqRJjzADAYqkXs/lVsc+AA==} engines: {node: '>= 10'} + '@ast-grep/napi@0.24.0': + resolution: {integrity: sha512-pldAub9mf12u/xJRNMj0x88y9uH2DpVHWqXfp+MoY1NT1j9jdA/QrXHHwddyeerfCoHZK1j5RDA23LaJAZloGQ==} + engines: {node: '>= 10'} + '@aws-crypto/crc32@3.0.0': resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} @@ -10912,11 +10990,6 @@ packages: resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==} engines: {node: '>=4'} - axios-retry@4.1.0: - resolution: {integrity: sha512-svdth4H00yhlsjBbjfLQ/sMLkXqeLxhiFC1nE1JtkN/CIssGxqk0UwTEdrVjwA2gr3yJkAulwvDSIm4z4HyPvg==} - peerDependencies: - axios: 0.x || 1.x - axios@1.6.8: resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} @@ -14667,9 +14740,9 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - nock@13.5.4: - resolution: {integrity: sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==} - engines: {node: '>= 10.13'} + nock@14.0.0-beta.7: + resolution: {integrity: sha512-+EQMm5W9K8YnBE2Ceg4hnJynaCZmvK8ZlFXQ2fxGwtkOkBUq8GpQLTks2m1jpvse9XDxMDDOHgOWpiznFuh0bA==} + engines: {node: '>= 18'} node-abi@3.62.0: resolution: {integrity: sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==} @@ -18070,24 +18143,45 @@ snapshots: '@ast-grep/cli-darwin-arm64@0.22.4': optional: true + '@ast-grep/cli-darwin-arm64@0.24.0': + optional: true + '@ast-grep/cli-darwin-x64@0.22.4': optional: true + '@ast-grep/cli-darwin-x64@0.24.0': + optional: true + '@ast-grep/cli-linux-arm64-gnu@0.22.4': optional: true + '@ast-grep/cli-linux-arm64-gnu@0.24.0': + optional: true + '@ast-grep/cli-linux-x64-gnu@0.22.4': optional: true + '@ast-grep/cli-linux-x64-gnu@0.24.0': + optional: true + '@ast-grep/cli-win32-arm64-msvc@0.22.4': optional: true + '@ast-grep/cli-win32-arm64-msvc@0.24.0': + optional: true + '@ast-grep/cli-win32-ia32-msvc@0.22.4': optional: true + '@ast-grep/cli-win32-ia32-msvc@0.24.0': + optional: true + '@ast-grep/cli-win32-x64-msvc@0.22.4': optional: true + '@ast-grep/cli-win32-x64-msvc@0.24.0': + optional: true + '@ast-grep/cli@0.22.4': dependencies: detect-libc: 2.0.3 @@ -18100,30 +18194,66 @@ snapshots: '@ast-grep/cli-win32-ia32-msvc': 0.22.4 '@ast-grep/cli-win32-x64-msvc': 0.22.4 + '@ast-grep/cli@0.24.0': + dependencies: + detect-libc: 2.0.3 + optionalDependencies: + '@ast-grep/cli-darwin-arm64': 0.24.0 + '@ast-grep/cli-darwin-x64': 0.24.0 + '@ast-grep/cli-linux-arm64-gnu': 0.24.0 + '@ast-grep/cli-linux-x64-gnu': 0.24.0 + '@ast-grep/cli-win32-arm64-msvc': 0.24.0 + '@ast-grep/cli-win32-ia32-msvc': 0.24.0 + '@ast-grep/cli-win32-x64-msvc': 0.24.0 + '@ast-grep/napi-darwin-arm64@0.22.4': optional: true + '@ast-grep/napi-darwin-arm64@0.24.0': + optional: true + '@ast-grep/napi-darwin-x64@0.22.4': optional: true + '@ast-grep/napi-darwin-x64@0.24.0': + optional: true + '@ast-grep/napi-linux-arm64-gnu@0.22.4': optional: true + '@ast-grep/napi-linux-arm64-gnu@0.24.0': + optional: true + '@ast-grep/napi-linux-x64-gnu@0.22.4': optional: true + '@ast-grep/napi-linux-x64-gnu@0.24.0': + optional: true + '@ast-grep/napi-linux-x64-musl@0.22.4': optional: true + '@ast-grep/napi-linux-x64-musl@0.24.0': + optional: true + '@ast-grep/napi-win32-arm64-msvc@0.22.4': optional: true + '@ast-grep/napi-win32-arm64-msvc@0.24.0': + optional: true + '@ast-grep/napi-win32-ia32-msvc@0.22.4': optional: true + '@ast-grep/napi-win32-ia32-msvc@0.24.0': + optional: true + '@ast-grep/napi-win32-x64-msvc@0.22.4': optional: true + '@ast-grep/napi-win32-x64-msvc@0.24.0': + optional: true + '@ast-grep/napi@0.22.4': optionalDependencies: '@ast-grep/napi-darwin-arm64': 0.22.4 @@ -18135,6 +18265,17 @@ snapshots: '@ast-grep/napi-win32-ia32-msvc': 0.22.4 '@ast-grep/napi-win32-x64-msvc': 0.22.4 + '@ast-grep/napi@0.24.0': + optionalDependencies: + '@ast-grep/napi-darwin-arm64': 0.24.0 + '@ast-grep/napi-darwin-x64': 0.24.0 + '@ast-grep/napi-linux-arm64-gnu': 0.24.0 + '@ast-grep/napi-linux-x64-gnu': 0.24.0 + '@ast-grep/napi-linux-x64-musl': 0.24.0 + '@ast-grep/napi-win32-arm64-msvc': 0.24.0 + '@ast-grep/napi-win32-ia32-msvc': 0.24.0 + '@ast-grep/napi-win32-x64-msvc': 0.24.0 + '@aws-crypto/crc32@3.0.0': dependencies: '@aws-crypto/util': 3.0.0 @@ -24354,11 +24495,6 @@ snapshots: axe-core@4.7.0: {} - axios-retry@4.1.0(axios@1.6.8): - dependencies: - axios: 1.6.8 - is-retry-allowed: 2.2.0 - axios@1.6.8: dependencies: follow-redirects: 1.15.6(debug@3.2.7) @@ -28965,13 +29101,10 @@ snapshots: lower-case: 2.0.2 tslib: 2.4.1 - nock@13.5.4: + nock@14.0.0-beta.7: dependencies: - debug: 4.3.4(supports-color@5.5.0) json-stringify-safe: 5.0.1 propagate: 2.0.1 - transitivePeerDependencies: - - supports-color node-abi@3.62.0: dependencies: From 3d427ec3b04614ad13bdd518f099ef9a035e5d0c Mon Sep 17 00:00:00 2001 From: Aleksy Rybicki <alekso.php@gmail.com> Date: Thu, 27 Jun 2024 11:47:26 +0100 Subject: [PATCH 3/4] fixed dependencies --- apps/modgpt/package.json | 3 ++- pnpm-lock.yaml | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/modgpt/package.json b/apps/modgpt/package.json index ebe689b9c..97823de42 100644 --- a/apps/modgpt/package.json +++ b/apps/modgpt/package.json @@ -21,7 +21,8 @@ "replicate": "0.25.2", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", - "valibot": "^0.24.1" + "valibot": "^0.24.1", + "@codemod-com/utilities": "workspace:*" }, "devDependencies": { "@types/node": "20.10.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 743dfd8e9..1aec12998 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -814,6 +814,9 @@ importers: apps/modgpt: dependencies: + '@codemod-com/utilities': + specifier: workspace:* + version: link:../../packages/utilities '@fastify/cors': specifier: 8.5.0 version: 8.5.0 @@ -4938,6 +4941,8 @@ importers: specifier: ^5.4.5 version: 5.4.5 + packages/database/generated/client: {} + packages/filemod: devDependencies: '@types/node': From 32b556b73954508eac3eb9c3455ea37881d64bd6 Mon Sep 17 00:00:00 2001 From: Aleksy Rybicki <alekso.php@gmail.com> Date: Thu, 27 Jun 2024 12:03:27 +0100 Subject: [PATCH 4/4] fixed tests --- apps/backend/src/publishHandler.test.ts | 48 ++++++++++++++----------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/apps/backend/src/publishHandler.test.ts b/apps/backend/src/publishHandler.test.ts index 1dbff7730..72be34731 100644 --- a/apps/backend/src/publishHandler.test.ts +++ b/apps/backend/src/publishHandler.test.ts @@ -20,8 +20,6 @@ const GET_USER_RETURN = { const MOCK_TIMESTAMP = "timestamp"; -const originalFetch = global.fetch; - const mocks = vi.hoisted(() => { const S3Client = vi.fn(); S3Client.prototype.send = vi.fn(); @@ -47,7 +45,7 @@ const mocks = vi.hoisted(() => { }, }, fetch: vi.fn().mockImplementation((url, options) => { - if (options.method === "GET") { + if (options.method === "GET" || !options.method) { return Promise.resolve({ json: () => Promise.resolve(GET_USER_RETURN), ok: true, @@ -220,26 +218,34 @@ describe("/publish route", async () => { requestTimeout: 5000, }); - expect(mocks.fetch).toHaveBeenCalledOnce(); - expect(mocks.fetch).toHaveBeenCalledWith( + expect(mocks.fetch).toHaveBeenCalledTimes(3); + expect(mocks.fetch).toHaveBeenNthCalledWith( + 2, "https://hooks.zapier.com/hooks/catch/18983913/2ybuovt/", { - codemod: { - name: codemodRcContents.name, - from: codemodRcContents.applicability?.from?.map((tuple) => - tuple.join(" "), - ), - to: codemodRcContents.applicability?.to?.map((tuple) => - tuple.join(" "), - ), - engine: codemodRcContents.engine, - publishedAt: MOCK_TIMESTAMP, - }, - author: { - username: GET_USER_RETURN.user.username, - name: `${GET_USER_RETURN.user.firstName} ${GET_USER_RETURN.user.lastName}`, - email: GET_USER_RETURN.user.emailAddresses[0]?.emailAddress, + body: JSON.stringify({ + codemod: { + name: codemodRcContents.name, + from: codemodRcContents.applicability?.from?.map((tuple) => + tuple.join(" "), + ), + to: codemodRcContents.applicability?.to?.map((tuple) => + tuple.join(" "), + ), + engine: codemodRcContents.engine, + publishedAt: MOCK_TIMESTAMP, + }, + author: { + username: GET_USER_RETURN.user.username, + name: `${GET_USER_RETURN.user.firstName} ${GET_USER_RETURN.user.lastName}`, + email: GET_USER_RETURN.user.emailAddresses[0]?.emailAddress, + }, + }), + headers: { + "Content-Type": "application/json", }, + method: "POST", + signal: expect.any(AbortSignal), }, ); @@ -609,6 +615,7 @@ describe("/publish route", async () => { mocks.prisma.codemodVersion.findFirst.mockImplementation(() => null); mocks.fetch.mockImplementation(() => ({ json: () => ({ ...GET_USER_RETURN, allowedNamespaces: ["org"] }), + ok: true, })); mocks.prisma.codemod.upsert.mockImplementation(() => { return { createdAt: { getTime: () => MOCK_TIMESTAMP }, id: "id" }; @@ -677,6 +684,7 @@ describe("/publish route", async () => { organizations: [], allowedNamespaces: [], }), + ok: true, })); const codemodRcContents: CodemodConfigInput = {