Skip to content

Commit 28aa4c0

Browse files
committed
fix: add deleted file response for pre-destructive
1 parent 6b28f91 commit 28aa4c0

2 files changed

Lines changed: 99 additions & 3 deletions

File tree

src/commands/project/deploy/start.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ import { DeployStages } from '../../../utils/deployStages.js';
2222
import { AsyncDeployResultFormatter } from '../../../formatters/asyncDeployResultFormatter.js';
2323
import { DeployResultFormatter } from '../../../formatters/deployResultFormatter.js';
2424
import { AsyncDeployResultJson, DeployResultJson, TestLevel } from '../../../utils/types.js';
25-
import { executeDeploy, resolveApi, validateTests, determineExitCode, buildDeployUrl } from '../../../utils/deploy.js';
25+
import {
26+
executeDeploy,
27+
resolveApi,
28+
validateTests,
29+
determineExitCode,
30+
buildDeployUrl,
31+
buildPreDestructiveFileResponses,
32+
} from '../../../utils/deploy.js';
2633
import { DeployCache } from '../../../utils/deployCache.js';
2734
import { DEPLOY_STATUS_CODES_DESCRIPTIONS } from '../../../utils/errorCodes.js';
2835
import { ConfigVars } from '../../../configMeta.js';
@@ -250,7 +257,7 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
250257
return Promise.resolve();
251258
});
252259

253-
const { deploy } = await executeDeploy(
260+
const { deploy, componentSet } = await executeDeploy(
254261
{
255262
...flags,
256263
'target-org': username,
@@ -268,6 +275,9 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
268275
throw new SfError('The deploy id is not available.');
269276
}
270277

278+
// Capture pre-destructive file responses BEFORE deploy executes
279+
const preDestructiveFileResponses = await buildPreDestructiveFileResponses(componentSet, project);
280+
271281
this.stages = new DeployStages({
272282
title,
273283
jsonEnabled: this.jsonEnabled(),
@@ -301,7 +311,8 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
301311
const result = await deploy.pollStatus({ timeout: flags.wait });
302312
process.exitCode = determineExitCode(result);
303313
this.stages.stop();
304-
const formatter = new DeployResultFormatter(result, flags, undefined, true);
314+
315+
const formatter = new DeployResultFormatter(result, flags, preDestructiveFileResponses, true);
305316

306317
if (!this.jsonEnabled()) {
307318
formatter.display();

src/utils/deploy.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { relative } from 'node:path';
1718
import { ConfigAggregator, Messages, Org, SfError, SfProject } from '@salesforce/core';
1819
import { Duration } from '@salesforce/kit';
1920
import { Nullable } from '@salesforce/ts-types';
2021
import {
2122
ComponentSet,
2223
ComponentSetBuilder,
24+
ComponentStatus,
2325
DeployResult,
26+
DestructiveChangesType,
27+
FileResponseSuccess,
2428
MetadataApiDeploy,
2529
MetadataApiDeployOptions,
2630
RegistryAccess,
@@ -254,3 +258,84 @@ export function buildDeployUrl(org: Org, deployId: string): string {
254258
const orgInstanceUrl = String(org.getField(Org.Fields.INSTANCE_URL));
255259
return `${orgInstanceUrl}/lightning/setup/DeployStatus/page?address=%2Fchangemgmt%2FmonitorDeploymentsDetails.apexp%3FasyncId%3D${deployId}%26retURL%3D%252Fchangemgmt%252FmonitorDeployment.apexp`;
256260
}
261+
262+
/**
263+
* Creates synthetic FileResponse objects for components in pre-destructive changes.
264+
* This ensures all file paths (e.g., .cls and .xml for ApexClass, or all LWC bundle files)
265+
* are shown in the deployment results table. This is needed because pre-destructive files
266+
* are deleted BEFORE the deploy, so getFileResponses() cannot access them.
267+
*
268+
* @param componentSet - The ComponentSet from the deployment (before deploy executes)
269+
* @param project - The SfProject to resolve file paths from
270+
* @returns Array of synthetic FileResponseSuccess objects representing pre-deleted files
271+
*/
272+
export async function buildPreDestructiveFileResponses(
273+
componentSet?: ComponentSet,
274+
project?: SfProject
275+
): Promise<FileResponseSuccess[]> {
276+
if (!componentSet || !project) {
277+
return [];
278+
}
279+
280+
const fileResponses: FileResponseSuccess[] = [];
281+
282+
// Get all source components and filter for pre-destructive ones
283+
const allComponents = componentSet.getSourceComponents().toArray();
284+
285+
const preDestructiveComponents = allComponents.filter(
286+
(component) => component.getDestructiveChangesType() === DestructiveChangesType.PRE
287+
);
288+
289+
if (preDestructiveComponents.length === 0) {
290+
return [];
291+
}
292+
293+
// Build metadata entries for ComponentSetBuilder
294+
const metadataEntries = preDestructiveComponents.map((comp) => `${comp.type.name}:${comp.fullName}`);
295+
296+
// Resolve the components from the project to get their file paths
297+
try {
298+
const resolvedComponentSet = await ComponentSetBuilder.build({
299+
metadata: {
300+
metadataEntries,
301+
directoryPaths: await getPackageDirs(),
302+
},
303+
projectDir: project.getPath(),
304+
});
305+
const resolvedComponents = resolvedComponentSet.getSourceComponents().toArray();
306+
307+
preDestructiveComponents.length = 0;
308+
preDestructiveComponents.push(...resolvedComponents);
309+
} catch (error) {
310+
// If this's not resolve, try to resolve with registry only
311+
}
312+
313+
for (const component of preDestructiveComponents) {
314+
// Get all file paths for this component (metadata XML + content files)
315+
const filePaths: string[] = [];
316+
const projectPath = project.getPath();
317+
318+
if (component.xml) {
319+
const relativePath = relative(projectPath, component.xml);
320+
filePaths.push(relativePath);
321+
}
322+
323+
// Add all content files (for bundles, this includes all files in the directory)
324+
const contentPaths = component.walkContent();
325+
for (const contentPath of contentPaths) {
326+
const relativePath = relative(projectPath, contentPath);
327+
filePaths.push(relativePath);
328+
}
329+
330+
for (const filePath of filePaths) {
331+
fileResponses.push({
332+
fullName: component.fullName,
333+
type: component.type.name,
334+
state: ComponentStatus.Deleted,
335+
filePath,
336+
});
337+
}
338+
}
339+
340+
return fileResponses;
341+
}

0 commit comments

Comments
 (0)