|
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 |
|
| 17 | +import { relative } from 'node:path'; |
17 | 18 | import { ConfigAggregator, Messages, Org, SfError, SfProject } from '@salesforce/core'; |
18 | 19 | import { Duration } from '@salesforce/kit'; |
19 | 20 | import { Nullable } from '@salesforce/ts-types'; |
20 | 21 | import { |
21 | 22 | ComponentSet, |
22 | 23 | ComponentSetBuilder, |
| 24 | + ComponentStatus, |
23 | 25 | DeployResult, |
| 26 | + DestructiveChangesType, |
| 27 | + FileResponseSuccess, |
24 | 28 | MetadataApiDeploy, |
25 | 29 | MetadataApiDeployOptions, |
26 | 30 | RegistryAccess, |
@@ -254,3 +258,84 @@ export function buildDeployUrl(org: Org, deployId: string): string { |
254 | 258 | const orgInstanceUrl = String(org.getField(Org.Fields.INSTANCE_URL)); |
255 | 259 | return `${orgInstanceUrl}/lightning/setup/DeployStatus/page?address=%2Fchangemgmt%2FmonitorDeploymentsDetails.apexp%3FasyncId%3D${deployId}%26retURL%3D%252Fchangemgmt%252FmonitorDeployment.apexp`; |
256 | 260 | } |
| 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