diff --git a/api/__tests__/projects.test.ts b/api/__tests__/projects.test.ts index b9dac3d..9c2920a 100644 --- a/api/__tests__/projects.test.ts +++ b/api/__tests__/projects.test.ts @@ -36,6 +36,8 @@ const createReadStreamMock = createReadStream as jest.MockedFunction< typeof createReadStream >; +const httpPostMock = http.post as jest.MockedFunction; + describe('api/projects', () => { const accountId = 999999; const projectId = 888888; @@ -132,6 +134,51 @@ describe('api/projects', () => { headers: { 'Content-Type': 'multipart/form-data' }, }); }); + + it('should call the v3 api when optional intermediateRepresentation is provided', async () => { + // @ts-expect-error Wants full axios response + httpPostMock.mockResolvedValue({ + data: { + createdBuildId: 123, + }, + }); + + const intermediateRepresentation = { + intermediateNodesIndexedByUid: { + 'calling-1': { + componentType: 'APP', + uid: 'calling-1', + config: {}, + componentDeps: {}, + files: {}, + }, + }, + }; + + await uploadProject( + accountId, + projectName, + projectFile, + uploadMessage, + platformVersion, + intermediateRepresentation + ); + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith(accountId, { + url: `project-components-external/v3/upload/new-api`, + timeout: 60_000, + data: { + projectFilesZip: formData, + platformVersion, + uploadRequest: JSON.stringify({ + ...intermediateRepresentation, + projectName, + buildMessage: uploadMessage, + }), + }, + headers: { 'Content-Type': 'multipart/form-data' }, + }); + }); }); describe('fetchProject', () => { diff --git a/api/projects.ts b/api/projects.ts index dd90d71..3bb385f 100644 --- a/api/projects.ts +++ b/api/projects.ts @@ -8,6 +8,7 @@ import { ProjectSettings, FetchPlatformVersionResponse, WarnLogsResponse, + UploadIRResponse, } from '../types/Project'; import { Build, FetchProjectBuildsResponse } from '../types/Build'; import { @@ -28,6 +29,8 @@ const PROJECTS_LOGS_API_PATH = 'dfs/logging/v1'; const DEVELOPER_PROJECTS_API_PATH = 'developer/projects/v1'; const MIGRATIONS_API_PATH = 'dfs/migrations/v1'; +const PROJECTS_V3_API_PATH = 'project-components-external/v3'; + export function fetchProjects( accountId: number ): HubSpotPromise { @@ -48,13 +51,38 @@ export function createProject( }); } -export function uploadProject( +export async function uploadProject( accountId: number, projectName: string, projectFile: string, uploadMessage: string, - platformVersion?: string -): HubSpotPromise { + platformVersion?: string, + intermediateRepresentation?: unknown +): HubSpotPromise { + if (intermediateRepresentation) { + const formData = { + projectFilesZip: fs.createReadStream(projectFile), + platformVersion, + uploadRequest: JSON.stringify({ + ...intermediateRepresentation, + projectName, + buildMessage: uploadMessage, + }), + }; + + const response = await http.post(accountId, { + url: `${PROJECTS_V3_API_PATH}/upload/new-api`, + timeout: 60_000, + data: formData, + headers: { 'Content-Type': 'multipart/form-data' }, + }); + + // Remap the response to match the expected shape + response.data.buildId = response.data.createdBuildId; + + return response; + } + const formData: FormData = { file: fs.createReadStream(projectFile), uploadMessage, @@ -62,7 +90,6 @@ export function uploadProject( if (platformVersion) { formData.platformVersion = platformVersion; } - return http.post(accountId, { url: `${PROJECTS_API_PATH}/upload/${encodeURIComponent(projectName)}`, timeout: 60_000, diff --git a/lib/fs.ts b/lib/fs.ts index 56a7bc0..c3e6ce9 100644 --- a/lib/fs.ts +++ b/lib/fs.ts @@ -42,12 +42,13 @@ export function flattenAndRemoveSymlinks( const generateRecursiveFilePromise = async ( dir: string, - file: string + file: string, + ignoreDirs?: string[] ): Promise => { return getFileInfoAsync(dir, file).then(fileData => { return new Promise(resolve => { if (fileData.type === STAT_TYPES.DIRECTORY) { - walk(fileData.filepath).then(files => { + walk(fileData.filepath, ignoreDirs).then(files => { resolve({ ...fileData, files }); }); } else { @@ -57,10 +58,17 @@ const generateRecursiveFilePromise = async ( }); }; -export async function walk(dir: string): Promise> { +export async function walk( + dir: string, + ignoreDirs?: string[] +): Promise> { function processFiles(files: Array) { + // If the directory is in the ignore list, return an empty array to skip the directory contents + if (ignoreDirs?.some(ignored => dir.includes(ignored))) { + return []; + } return Promise.all( - files.map(file => generateRecursiveFilePromise(dir, file)) + files.map(file => generateRecursiveFilePromise(dir, file, ignoreDirs)) ); } diff --git a/types/Project.ts b/types/Project.ts index 7835fd6..a30696d 100644 --- a/types/Project.ts +++ b/types/Project.ts @@ -38,6 +38,11 @@ export type UploadProjectResponse = { }; }; +export type UploadIRResponse = { + buildId?: number; + createdBuildId: number; +}; + export type ProjectSettings = { isAutoDeployEnabled: boolean; };