Skip to content

Commit

Permalink
Merge pull request #273 from forcedotcom/sh/partial-bundle-delete
Browse files Browse the repository at this point in the history
fix: treat ExperienceBundles and StaticResources like bundles for partial delete
  • Loading branch information
WillieRuemmele authored Nov 22, 2022
2 parents 77557f4 + aaa6c38 commit ab5e1c5
Show file tree
Hide file tree
Showing 24 changed files with 466 additions and 50 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"dependencies": {
"@salesforce/core": "^3.31.19",
"@salesforce/kit": "^1.8.0",
"@salesforce/source-deploy-retrieve": "^7.5.9",
"@salesforce/source-deploy-retrieve": "^7.5.12",
"graceful-fs": "^4.2.10",
"isomorphic-git": "1.17.0",
"ts-retry-promise": "^0.7.0"
Expand Down Expand Up @@ -84,4 +84,4 @@
"publishConfig": {
"access": "public"
}
}
}
7 changes: 4 additions & 3 deletions src/shared/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { isString } from '@salesforce/ts-types';
import { SourceComponent } from '@salesforce/source-deploy-retrieve';
import { RemoteChangeElement, ChangeResult } from './types';

export const getMetadataKey = (metadataType: string, metadataName: string): string => `${metadataType}__${metadataName}`;
export const getMetadataKey = (metadataType: string, metadataName: string): string =>
`${metadataType}__${metadataName}`;

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

export const isBundle = (cmp: SourceComponent): boolean =>
cmp.type.strategies?.adapter === 'bundle' || cmp.type.strategies?.adapter === 'digitalExperience';
export const supportsPartialDelete = (cmp: SourceComponent): boolean => !!cmp.type.supportsPartialDelete;

export const isLwcLocalOnlyTest = (filePath: string): boolean =>
filePath.includes('__utam__') || filePath.includes('__tests__');

Expand Down
10 changes: 6 additions & 4 deletions src/shared/localComponentSetArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
DestructiveChangesType,
} from '@salesforce/source-deploy-retrieve';
import { sourceComponentGuard } from './guards';
import { isBundle, pathIsInFolder } from './functions';
import { supportsPartialDelete, pathIsInFolder } from './functions';

interface GroupedFileInput {
packageDirs: NamedPackageDir[];
Expand All @@ -27,7 +27,8 @@ interface GroupedFile {
deletes: string[];
}

export const getGroupedFiles = (input: GroupedFileInput, byPackageDir = false): GroupedFile[] => (byPackageDir ? getSequential(input) : getNonSequential(input)).filter(
export const getGroupedFiles = (input: GroupedFileInput, byPackageDir = false): GroupedFile[] =>
(byPackageDir ? getSequential(input) : getNonSequential(input)).filter(
(group) => group.deletes.length || group.nonDeletes.length
);

Expand Down Expand Up @@ -75,8 +76,9 @@ export const getComponentSets = (groupings: GroupedFile[], sourceApiVersion?: st
.flatMap((filename) => resolverForDeletes.getComponentsFromPath(filename))
.filter(sourceComponentGuard)
.map((component) => {
// 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
if (isBundle(component) && component.content && fs.existsSync(component.content)) {
// if the component supports partial delete AND there are files that are not deleted,
// set the component for deploy, not for delete.
if (supportsPartialDelete(component) && component.content && fs.existsSync(component.content)) {
// all bundle types have a directory name
try {
resolverForNonDeletes
Expand Down
4 changes: 2 additions & 2 deletions src/sourceTracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
RemoteChangeElement,
} from './shared/types';
import { sourceComponentGuard } from './shared/guards';
import { isBundle, pathIsInFolder, ensureRelative } from './shared/functions';
import { supportsPartialDelete, pathIsInFolder, ensureRelative } from './shared/functions';
import { registrySupportsType } from './shared/metadataKeys';
import { hasSfdxTrackingFiles } from './compatibility';
import { populateFilePaths } from './shared/populateFilePaths';
Expand Down Expand Up @@ -403,7 +403,7 @@ export class SourceTracking extends AsyncCreatable {
const bundlesWithDeletedFiles = (
await this.getChanges({ origin: 'local', state: 'delete', format: 'SourceComponent' })
)
.filter(isBundle)
.filter(supportsPartialDelete)
.filter((cmp) => deployedFilesAsVirtualComponentSet.has({ type: cmp.type, fullName: cmp.fullName }))
.map((cmp) => cmp.content)
.filter(isString);
Expand Down
170 changes: 170 additions & 0 deletions test/nuts/local/partialBundleDelete.nut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import * as path from 'path';
import * as fs from 'fs';
import { TestSession } from '@salesforce/cli-plugins-testkit';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { getComponentSets } from '../../../src/shared/localComponentSetArray';

describe('Bundle-like types delete', () => {
let session: TestSession;

before(async () => {
session = await TestSession.create({
project: {
sourceDir: path.join('test', 'nuts', 'repros', 'partialBundleDelete'),
},
authStrategy: 'NONE',
});
});

// We need a sinon sandbox to stub the file system to make it look like we
// deleted some files.
const sandbox = sinon.createSandbox();

after(async () => {
await session?.clean();
});

afterEach(() => {
sandbox.restore();
});

it('returns components for deploy with partial LWC delete', () => {
const lwcTestCompDir = path.join(session.project.dir, 'force-app', 'lwc', 'testComp');
const lwcHtmlFile = path.join(lwcTestCompDir, 'myComp.html');
const lwcJsFile = path.join(lwcTestCompDir, 'myComp.js');
const lwcMetaFile = path.join(lwcTestCompDir, 'myComp.js-meta.xml');

const compSets = getComponentSets([
{
path: path.join(session.project.dir, 'force-app', 'lwc'),
nonDeletes: [lwcJsFile, lwcMetaFile],
deletes: [lwcHtmlFile],
},
]);

expect(compSets.length).to.equal(1);
compSets.forEach((cs) => {
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal([]);
const comps = cs.getSourceComponents().toArray();
expect(comps[0].isMarkedForDelete()).to.equal(false);
});
});

it('returns components for delete for full LWC delete', () => {
// We stub this so it appears that we deleted all the LWC files
sandbox.stub(fs, 'existsSync').returns(false);
const lwcTestCompDir = path.join(session.project.dir, 'force-app', 'lwc', 'testComp');
const lwcHtmlFile = path.join(lwcTestCompDir, 'myComp.html');
const lwcJsFile = path.join(lwcTestCompDir, 'myComp.js');
const lwcMetaFile = path.join(lwcTestCompDir, 'myComp.js-meta.xml');

const compSets = getComponentSets([
{
path: path.join(session.project.dir, 'force-app', 'lwc'),
nonDeletes: [],
deletes: [lwcHtmlFile, lwcJsFile, lwcMetaFile],
},
]);

expect(compSets.length).to.equal(1);
compSets.forEach((cs) => {
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal(['post']);
const comps = cs.getSourceComponents().toArray();
expect(comps[0].isMarkedForDelete()).to.equal(true);
});
});

it('returns components for deploy with partial StaticResource delete', () => {
const srDir = path.join(session.project.dir, 'force-app', 'staticresources');
const srFile1 = path.join(srDir, 'ZippedResource', 'file1.json');
const srFile2 = path.join(srDir, 'ZippedResource', 'file2.json');
const srMetaFile = path.join(srDir, 'ZippedResource.resource-meta.xml');

const compSets = getComponentSets([
{
path: srDir,
nonDeletes: [srMetaFile, srFile2],
deletes: [srFile1],
},
]);

expect(compSets.length).to.equal(1);
compSets.forEach((cs) => {
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal([]);
const comps = cs.getSourceComponents().toArray();
expect(comps[0].isMarkedForDelete()).to.equal(false);
});
});

it('returns components for delete for full StaticResource delete', () => {
// We stub this so it appears that we deleted all the ZippedResource static resource files
sandbox.stub(fs, 'existsSync').returns(false);
const srDir = path.join(session.project.dir, 'force-app', 'staticresources');
const srFile1 = path.join(srDir, 'ZippedResource', 'file1.json');
const srFile2 = path.join(srDir, 'ZippedResource', 'file2.json');
const srMetaFile = path.join(srDir, 'ZippedResource.resource-meta.xml');

const compSets = getComponentSets([
{
path: srDir,
nonDeletes: [],
deletes: [srFile1, srFile2, srMetaFile],
},
]);

expect(compSets.length).to.equal(1);
compSets.forEach((cs) => {
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal(['post']);
const comps = cs.getSourceComponents().toArray();
expect(comps[0].isMarkedForDelete()).to.equal(true);
});
});

it('returns components for deploy with partial DigitalExperienceBundle delete', () => {
const debDir = path.join(session.project.dir, 'force-app', 'digitalExperiences', 'site', 'Xcel_Energy1');
const deFile1 = path.join(debDir, 'sfdc_cms__view', 'home', 'content.json');

const compSets = getComponentSets([
{
path: debDir,
nonDeletes: [],
deletes: [deFile1],
},
]);

expect(compSets.length).to.equal(1);
compSets.forEach((cs) => {
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal([]);
const comps = cs.getSourceComponents().toArray();
expect(comps[0].isMarkedForDelete()).to.equal(false);
});
});

it('returns components for deploy with partial ExperienceBundle delete', () => {
const ebDir = path.join(session.project.dir, 'force-app', 'experiences', 'fooEB');
const eFile1 = path.join(ebDir, 'views', 'login.json');
const eFile2 = path.join(ebDir, 'routes', 'login.json');

const compSets = getComponentSets([
{
path: ebDir,
nonDeletes: [eFile2],
deletes: [eFile1],
},
]);

expect(compSets.length).to.equal(1);
compSets.forEach((cs) => {
expect(cs.getTypesOfDestructiveChanges()).to.deep.equal([]);
const comps = cs.getSourceComponents().toArray();
expect(comps[0].isMarkedForDelete()).to.equal(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<DigitalExperienceBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Xcel_Energy1</label>
</DigitalExperienceBundle>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"apiName": "error",
"type": "sfdc_cms__view",
"path": "views"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"type": "sfdc_cms__view",
"title": "Error",
"contentBody": {
"sfdc_cms:component": {
"definitionName": "community_layout:sldsFlexibleLayout",
"sfdc_cms:children": [
{
"regionName": "content",
"sfdc_cms:children": [
{
"componentAttributes": {
"backgroundImageConfig": "",
"backgroundImageOverlay": "rgba(0,0,0,0)",
"sectionConfig": "{\"UUID\":\"66609d49-c588-40c5-a8d7-755443ed9fb4\",\"columns\":[{\"UUID\":\"b1a85d75-a44b-4392-9ded-5d56b382d087\",\"columnName\":\"Column 1\",\"columnKey\":\"col1\",\"columnWidth\":\"12\",\"seedComponents\":null}]}"
},
"definitionName": "community_layout:section",
"sfdc_cms:children": [
{
"regionName": "col1",
"sfdc_cms:children": [
{
"componentAttributes": {
"richTextValue": "<h1 style=\"text-align: center;\">Invalid Page</h1>"
},
"definitionName": "community_builder:richTextEditor",
"sfdc_cms:id": "f4b7e5ae-83fc-47f7-a5ba-025f3d9bd2b4",
"sfdc_cms:type": "component"
}
],
"sfdc_cms:id": "b1a85d75-a44b-4392-9ded-5d56b382d087",
"sfdc_cms:type": "region",
"title": "Column 1"
}
],
"sfdc_cms:id": "66609d49-c588-40c5-a8d7-755443ed9fb4",
"sfdc_cms:type": "component"
}
],
"sfdc_cms:id": "84eb2730-80e9-480b-9a63-e7beda32e9cf",
"sfdc_cms:type": "region",
"title": "Content"
},
{
"regionName": "sfdcHiddenRegion",
"sfdc_cms:children": [
{
"componentAttributes": {
"customHeadTags": "",
"description": "",
"pageTitle": "Error",
"recordId": "{!recordId}"
},
"definitionName": "community_builder:seoAssistant",
"sfdc_cms:id": "fe045efd-2e45-495b-b7e2-d21d6f586314",
"sfdc_cms:type": "component"
}
],
"sfdc_cms:id": "4352728a-3324-4524-8bd2-a4074d544541",
"sfdc_cms:type": "region",
"title": "sfdcHiddenRegion"
}
],
"sfdc_cms:id": "270041de-95c7-41e5-b70f-918878f71f68",
"sfdc_cms:type": "component"
},
"themeLayoutType": "Inner",
"title": "Error",
"viewType": "error"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"apiName": "home",
"type": "sfdc_cms__view",
"path": "views"
}
Loading

0 comments on commit ab5e1c5

Please sign in to comment.