Skip to content

Commit cdac022

Browse files
committed
fix: treat ExperienceBundles and StaticResources like bundles for partial delete
1 parent 7d060f2 commit cdac022

File tree

21 files changed

+437
-5
lines changed

21 files changed

+437
-5
lines changed

src/shared/functions.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { isString } from '@salesforce/ts-types';
1010
import { SourceComponent } from '@salesforce/source-deploy-retrieve';
1111
import { RemoteChangeElement, ChangeResult } from './types';
1212

13-
export const getMetadataKey = (metadataType: string, metadataName: string): string => `${metadataType}__${metadataName}`;
13+
export const getMetadataKey = (metadataType: string, metadataName: string): string =>
14+
`${metadataType}__${metadataName}`;
1415

1516
export const getKeyFromObject = (element: RemoteChangeElement | ChangeResult): string => {
1617
if (element.type && element.name) {
@@ -19,8 +20,17 @@ export const getKeyFromObject = (element: RemoteChangeElement | ChangeResult): s
1920
throw new Error(`unable to complete key from ${JSON.stringify(element)}`);
2021
};
2122

22-
export const isBundle = (cmp: SourceComponent): boolean =>
23-
cmp.type.strategies?.adapter === 'bundle' || cmp.type.strategies?.adapter === 'digitalExperience';
23+
// Return whether a component is part of a bundle. Note that this applies to SDR bundle
24+
// types, but it also applies to some special types that are not technically classified
25+
// in SDR as bundles, such as DigitalExperienceBundle, ExperienceBundle, and StaticResources.
26+
// These types share characteristics of bundle types.
27+
export const isBundle = (cmp: SourceComponent): boolean => {
28+
const cmpTypeAdapter = cmp.type.strategies?.adapter;
29+
const cmpTypeName = cmp.type.name;
30+
const bundleLikeTypes = ['DigitalExperience', 'DigitalExperienceBundle', 'ExperienceBundle', 'StaticResource'];
31+
return cmpTypeAdapter === 'bundle' || bundleLikeTypes.includes(cmpTypeName);
32+
};
33+
2434
export const isLwcLocalOnlyTest = (filePath: string): boolean =>
2535
filePath.includes('__utam__') || filePath.includes('__tests__');
2636

src/shared/localComponentSetArray.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ interface GroupedFile {
2727
deletes: string[];
2828
}
2929

30-
export const getGroupedFiles = (input: GroupedFileInput, byPackageDir = false): GroupedFile[] => (byPackageDir ? getSequential(input) : getNonSequential(input)).filter(
30+
export const getGroupedFiles = (input: GroupedFileInput, byPackageDir = false): GroupedFile[] =>
31+
(byPackageDir ? getSequential(input) : getNonSequential(input)).filter(
3132
(group) => group.deletes.length || group.nonDeletes.length
3233
);
3334

@@ -75,7 +76,8 @@ export const getComponentSets = (groupings: GroupedFile[], sourceApiVersion?: st
7576
.flatMap((filename) => resolverForDeletes.getComponentsFromPath(filename))
7677
.filter(sourceComponentGuard)
7778
.map((component) => {
78-
// if the component is a file in a bundle type AND there are files from the bundle that are not deleted, set the bundle for deploy, not for delete
79+
// if the component is part of a bundle AND there are files from the bundle that are not deleted,
80+
// set the bundle for deploy, not for delete.
7981
if (isBundle(component) && component.content && fs.existsSync(component.content)) {
8082
// all bundle types have a directory name
8183
try {
+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright (c) 2020, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import * as path from 'path';
8+
import * as fs from 'fs';
9+
import { TestSession } from '@salesforce/cli-plugins-testkit';
10+
import { expect } from 'chai';
11+
import * as sinon from 'sinon';
12+
import { getComponentSets } from '../../../src/shared/localComponentSetArray';
13+
14+
describe('Bundle-like types delete', () => {
15+
let session: TestSession;
16+
17+
before(async () => {
18+
session = await TestSession.create({
19+
project: {
20+
sourceDir: path.join('test', 'nuts', 'repros', 'partialBundleDelete'),
21+
},
22+
authStrategy: 'NONE',
23+
});
24+
});
25+
26+
// We need a sinon sandbox to stub the file system to make it look like we
27+
// deleted some files.
28+
const sandbox = sinon.createSandbox();
29+
30+
after(async () => {
31+
await session?.clean();
32+
});
33+
34+
afterEach(() => {
35+
sandbox.restore();
36+
});
37+
38+
it('returns components for deploy with partial LWC delete', () => {
39+
const lwcTestCompDir = path.join(session.project.dir, 'force-app', 'lwc', 'testComp');
40+
const lwcHtmlFile = path.join(lwcTestCompDir, 'myComp.html');
41+
const lwcJsFile = path.join(lwcTestCompDir, 'myComp.js');
42+
const lwcMetaFile = path.join(lwcTestCompDir, 'myComp.js-meta.xml');
43+
44+
const compSets = getComponentSets([
45+
{
46+
path: path.join(session.project.dir, 'force-app', 'lwc'),
47+
nonDeletes: [lwcJsFile, lwcMetaFile],
48+
deletes: [lwcHtmlFile],
49+
},
50+
]);
51+
52+
expect(compSets.length).to.equal(1);
53+
compSets.forEach((cs) => {
54+
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal([]);
55+
const comps = cs.getSourceComponents().toArray();
56+
expect(comps[0].isMarkedForDelete()).to.equal(false);
57+
});
58+
});
59+
60+
it('returns components for delete for full LWC delete', () => {
61+
// We stub this so it appears that we deleted all the LWC files
62+
sandbox.stub(fs, 'existsSync').returns(false);
63+
const lwcTestCompDir = path.join(session.project.dir, 'force-app', 'lwc', 'testComp');
64+
const lwcHtmlFile = path.join(lwcTestCompDir, 'myComp.html');
65+
const lwcJsFile = path.join(lwcTestCompDir, 'myComp.js');
66+
const lwcMetaFile = path.join(lwcTestCompDir, 'myComp.js-meta.xml');
67+
68+
const compSets = getComponentSets([
69+
{
70+
path: path.join(session.project.dir, 'force-app', 'lwc'),
71+
nonDeletes: [],
72+
deletes: [lwcHtmlFile, lwcJsFile, lwcMetaFile],
73+
},
74+
]);
75+
76+
expect(compSets.length).to.equal(1);
77+
compSets.forEach((cs) => {
78+
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal(['post']);
79+
const comps = cs.getSourceComponents().toArray();
80+
expect(comps[0].isMarkedForDelete()).to.equal(true);
81+
});
82+
});
83+
84+
it('returns components for deploy with partial StaticResource delete', () => {
85+
const srDir = path.join(session.project.dir, 'force-app', 'staticresources');
86+
const srFile1 = path.join(srDir, 'ZippedResource', 'file1.json');
87+
const srFile2 = path.join(srDir, 'ZippedResource', 'file2.json');
88+
const srMetaFile = path.join(srDir, 'ZippedResource.resource-meta.xml');
89+
90+
const compSets = getComponentSets([
91+
{
92+
path: srDir,
93+
nonDeletes: [srMetaFile, srFile2],
94+
deletes: [srFile1],
95+
},
96+
]);
97+
98+
expect(compSets.length).to.equal(1);
99+
compSets.forEach((cs) => {
100+
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal([]);
101+
const comps = cs.getSourceComponents().toArray();
102+
expect(comps[0].isMarkedForDelete()).to.equal(false);
103+
});
104+
});
105+
106+
it('returns components for delete for full StaticResource delete', () => {
107+
// We stub this so it appears that we deleted all the ZippedResource static resource files
108+
sandbox.stub(fs, 'existsSync').returns(false);
109+
const srDir = path.join(session.project.dir, 'force-app', 'staticresources');
110+
const srFile1 = path.join(srDir, 'ZippedResource', 'file1.json');
111+
const srFile2 = path.join(srDir, 'ZippedResource', 'file2.json');
112+
const srMetaFile = path.join(srDir, 'ZippedResource.resource-meta.xml');
113+
114+
const compSets = getComponentSets([
115+
{
116+
path: srDir,
117+
nonDeletes: [],
118+
deletes: [srFile1, srFile2, srMetaFile],
119+
},
120+
]);
121+
122+
expect(compSets.length).to.equal(1);
123+
compSets.forEach((cs) => {
124+
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal(['post']);
125+
const comps = cs.getSourceComponents().toArray();
126+
expect(comps[0].isMarkedForDelete()).to.equal(true);
127+
});
128+
});
129+
130+
it('returns components for deploy with partial DigitalExperienceBundle delete', () => {
131+
const debDir = path.join(session.project.dir, 'force-app', 'digitalExperiences', 'site', 'Xcel_Energy1');
132+
const deFile1 = path.join(debDir, 'sfdc_cms__view', 'home', 'content.json');
133+
134+
const compSets = getComponentSets([
135+
{
136+
path: debDir,
137+
nonDeletes: [],
138+
deletes: [deFile1],
139+
},
140+
]);
141+
142+
expect(compSets.length).to.equal(1);
143+
compSets.forEach((cs) => {
144+
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal([]);
145+
const comps = cs.getSourceComponents().toArray();
146+
expect(comps[0].isMarkedForDelete()).to.equal(false);
147+
});
148+
});
149+
150+
it('returns components for deploy with partial ExperienceBundle delete', () => {
151+
const ebDir = path.join(session.project.dir, 'force-app', 'experiences', 'fooEB');
152+
const eFile1 = path.join(ebDir, 'views', 'login.json');
153+
const eFile2 = path.join(ebDir, 'routes', 'login.json');
154+
155+
const compSets = getComponentSets([
156+
{
157+
path: ebDir,
158+
nonDeletes: [eFile2],
159+
deletes: [eFile1],
160+
},
161+
]);
162+
163+
expect(compSets.length).to.equal(1);
164+
compSets.forEach((cs) => {
165+
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal([]);
166+
const comps = cs.getSourceComponents().toArray();
167+
expect(comps[0].isMarkedForDelete()).to.equal(false);
168+
});
169+
});
170+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<DigitalExperienceBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<label>Xcel_Energy1</label>
4+
</DigitalExperienceBundle>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"apiName": "error",
3+
"type": "sfdc_cms__view",
4+
"path": "views"
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"type": "sfdc_cms__view",
3+
"title": "Error",
4+
"contentBody": {
5+
"sfdc_cms:component": {
6+
"definitionName": "community_layout:sldsFlexibleLayout",
7+
"sfdc_cms:children": [
8+
{
9+
"regionName": "content",
10+
"sfdc_cms:children": [
11+
{
12+
"componentAttributes": {
13+
"backgroundImageConfig": "",
14+
"backgroundImageOverlay": "rgba(0,0,0,0)",
15+
"sectionConfig": "{\"UUID\":\"66609d49-c588-40c5-a8d7-755443ed9fb4\",\"columns\":[{\"UUID\":\"b1a85d75-a44b-4392-9ded-5d56b382d087\",\"columnName\":\"Column 1\",\"columnKey\":\"col1\",\"columnWidth\":\"12\",\"seedComponents\":null}]}"
16+
},
17+
"definitionName": "community_layout:section",
18+
"sfdc_cms:children": [
19+
{
20+
"regionName": "col1",
21+
"sfdc_cms:children": [
22+
{
23+
"componentAttributes": {
24+
"richTextValue": "<h1 style=\"text-align: center;\">Invalid Page</h1>"
25+
},
26+
"definitionName": "community_builder:richTextEditor",
27+
"sfdc_cms:id": "f4b7e5ae-83fc-47f7-a5ba-025f3d9bd2b4",
28+
"sfdc_cms:type": "component"
29+
}
30+
],
31+
"sfdc_cms:id": "b1a85d75-a44b-4392-9ded-5d56b382d087",
32+
"sfdc_cms:type": "region",
33+
"title": "Column 1"
34+
}
35+
],
36+
"sfdc_cms:id": "66609d49-c588-40c5-a8d7-755443ed9fb4",
37+
"sfdc_cms:type": "component"
38+
}
39+
],
40+
"sfdc_cms:id": "84eb2730-80e9-480b-9a63-e7beda32e9cf",
41+
"sfdc_cms:type": "region",
42+
"title": "Content"
43+
},
44+
{
45+
"regionName": "sfdcHiddenRegion",
46+
"sfdc_cms:children": [
47+
{
48+
"componentAttributes": {
49+
"customHeadTags": "",
50+
"description": "",
51+
"pageTitle": "Error",
52+
"recordId": "{!recordId}"
53+
},
54+
"definitionName": "community_builder:seoAssistant",
55+
"sfdc_cms:id": "fe045efd-2e45-495b-b7e2-d21d6f586314",
56+
"sfdc_cms:type": "component"
57+
}
58+
],
59+
"sfdc_cms:id": "4352728a-3324-4524-8bd2-a4074d544541",
60+
"sfdc_cms:type": "region",
61+
"title": "sfdcHiddenRegion"
62+
}
63+
],
64+
"sfdc_cms:id": "270041de-95c7-41e5-b70f-918878f71f68",
65+
"sfdc_cms:type": "component"
66+
},
67+
"themeLayoutType": "Inner",
68+
"title": "Error",
69+
"viewType": "error"
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"apiName": "home",
3+
"type": "sfdc_cms__view",
4+
"path": "views"
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"type": "sfdc_cms__view",
3+
"title": "Home",
4+
"contentBody": {
5+
"sfdc_cms:component": {
6+
"definitionName": "community_layout:sldsFlexibleLayout",
7+
"sfdc_cms:children": [
8+
{
9+
"regionName": "content",
10+
"sfdc_cms:children": [
11+
{
12+
"componentAttributes": {
13+
"backgroundImageConfig": "",
14+
"backgroundImageOverlay": "rgba(0,0,0,0)",
15+
"sectionConfig": "{\"UUID\":\"15aa9710-b6ef-4ea0-b0c6-ba180c1c3c2e\",\"columns\":[{\"UUID\":\"be38dd97-d7ad-4fe8-a0a2-69e4a530bd64\",\"columnName\":\"Column 1\",\"columnKey\":\"col1\",\"columnWidth\":\"12\",\"seedComponents\":null}]}"
16+
},
17+
"definitionName": "community_layout:section",
18+
"sfdc_cms:children": [
19+
{
20+
"regionName": "col1",
21+
"sfdc_cms:children": [
22+
{
23+
"componentAttributes": {
24+
"backgroundColor": "",
25+
"paddingHorizontal": "none",
26+
"paddingVertical": "none",
27+
"text": "Energy Savings in United States",
28+
"textAlign": "left",
29+
"textDecoration": "{}",
30+
"textDisplayInfo": "{}"
31+
},
32+
"definitionName": "dxp_base:textBlock",
33+
"sfdc_cms:id": "48307a75-c62b-4773-83d0-80bafd21a072",
34+
"sfdc_cms:type": "component"
35+
}
36+
],
37+
"sfdc_cms:id": "be38dd97-d7ad-4fe8-a0a2-69e4a530bd64",
38+
"sfdc_cms:type": "region",
39+
"title": "Column 1"
40+
}
41+
],
42+
"sfdc_cms:id": "15aa9710-b6ef-4ea0-b0c6-ba180c1c3c2e",
43+
"sfdc_cms:type": "component"
44+
}
45+
],
46+
"sfdc_cms:id": "5e0b2fb6-7fd0-4ef7-90da-f9df55c08314",
47+
"sfdc_cms:type": "region",
48+
"title": "Content"
49+
},
50+
{
51+
"regionName": "sfdcHiddenRegion",
52+
"sfdc_cms:children": [
53+
{
54+
"componentAttributes": {
55+
"customHeadTags": "",
56+
"description": "",
57+
"pageTitle": "Home",
58+
"recordId": "{!recordId}"
59+
},
60+
"definitionName": "community_builder:seoAssistant",
61+
"sfdc_cms:id": "3044250c-1fb2-4dfd-bd08-f3ee59e53780",
62+
"sfdc_cms:type": "component"
63+
}
64+
],
65+
"sfdc_cms:id": "bdcdd407-ca9c-49cc-9f47-9f2bd6f4d707",
66+
"sfdc_cms:type": "region",
67+
"title": "sfdcHiddenRegion"
68+
}
69+
],
70+
"sfdc_cms:id": "86c550f0-05de-4ad9-ad11-a1714d30703f",
71+
"sfdc_cms:type": "component"
72+
},
73+
"themeLayoutType": "Inner",
74+
"title": "Home",
75+
"viewType": "home"
76+
}
77+
}

0 commit comments

Comments
 (0)