Skip to content

Commit dd0eddb

Browse files
committed
feat: Support web app bundle @W-20091482
1 parent 3707ec6 commit dd0eddb

File tree

6 files changed

+249
-1
lines changed

6 files changed

+249
-1
lines changed

src/client/deployMessages.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,30 @@ export const createResponses = (component: SourceComponent, responseMessages: De
8787
if (state === ComponentStatus.Failed) {
8888
return [{ ...base, state, ...parseDeployDiagnostic(component, message) } satisfies FileResponseFailure];
8989
} else {
90+
const isWebAppBundle = component.type.name === 'DigitalExperienceBundle' &&
91+
component.fullName.startsWith('web_app/') &&
92+
component.content;
93+
94+
if (isWebAppBundle) {
95+
const walkedPaths = component.walkContent();
96+
const bundleResponse: FileResponseSuccess = {
97+
fullName: component.fullName,
98+
type: component.type.name,
99+
state,
100+
filePath: component.content!,
101+
};
102+
const fileResponses: FileResponseSuccess[] = walkedPaths.map((filePath) => {
103+
const relPath = filePath.replace(component.content! + '/', '');
104+
return {
105+
fullName: `${component.fullName}/${relPath}`,
106+
type: 'DigitalExperience',
107+
state,
108+
filePath,
109+
};
110+
});
111+
return [bundleResponse, ...fileResponses];
112+
}
113+
90114
return [
91115
...(shouldWalkContent(component)
92116
? component.walkContent().map((filePath): FileResponseSuccess => ({ ...base, state, filePath }))

src/resolve/adapters/digitalExperienceSourceAdapter.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import { BundleSourceAdapter } from './bundleSourceAdapter';
2525

2626
Messages.importMessagesDirectory(__dirname);
2727
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
28+
29+
// Constants for DigitalExperience base types
30+
const WEB_APP_BASE_TYPE = 'web_app';
31+
2832
/**
2933
* Source Adapter for DigitalExperience metadata types. This metadata type is a bundled type of the format
3034
*
@@ -58,18 +62,57 @@ const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sd
5862
* content/
5963
* ├── bars/
6064
* | ├── bars.digitalExperience-meta.xml
65+
* web_app/
66+
* ├── zenith/
67+
* | ├── css/
68+
* | | ├── header/
69+
* | | | ├── header.css
70+
* | | ├── home.css
71+
* | ├── js/
72+
* | | ├── home.js
73+
* | ├── html/
74+
* | | ├── home.html
75+
* | ├── images/
76+
* | | ├── logos/
77+
* | | | ├── logo.png
6178
* ```
6279
*
6380
* In the above structure the metadata xml file ending with "digitalExperience-meta.xml" belongs to DigitalExperienceBundle MD type.
6481
* The "_meta.json" files are child metadata files of DigitalExperienceBundle belonging to DigitalExperience MD type. The rest of the files in the
6582
* corresponding folder are the contents to the DigitalExperience metadata. So, incase of DigitalExperience the metadata file is a JSON file
66-
* and not an XML file
83+
* and not an XML file.
84+
*
85+
* For web_app base type, the bundle is identified by directory structure alone without metadata XML files.
6786
*/
6887
export class DigitalExperienceSourceAdapter extends BundleSourceAdapter {
88+
public getComponent(path: SourcePath, isResolvingSource = true): SourceComponent | undefined {
89+
if (this.isBundleType() && isWebAppBaseType(path) && this.tree.isDirectory(path)) {
90+
const pathParts = path.split(sep);
91+
const bundleNameIndex = getDigitalExperiencesIndex(path) + 2;
92+
if (bundleNameIndex === pathParts.length - 1) {
93+
return this.populate(path, undefined);
94+
}
95+
}
96+
return super.getComponent(path, isResolvingSource);
97+
}
98+
99+
protected parseAsRootMetadataXml(path: string): MetadataXml | undefined {
100+
if (isWebAppBaseType(path)) {
101+
return undefined;
102+
}
103+
if (!this.isBundleType() && !path.endsWith(this.type.metaFileSuffix ?? '_meta.json')) {
104+
return undefined;
105+
}
106+
return super.parseAsRootMetadataXml(path);
107+
}
108+
69109
protected getRootMetadataXmlPath(trigger: string): string {
70110
if (this.isBundleType()) {
71111
return this.getBundleMetadataXmlPath(trigger);
72112
}
113+
if (isWebAppBaseType(trigger)) {
114+
return '';
115+
}
73116
// metafile name = metaFileSuffix for DigitalExperience.
74117
if (!this.type.metaFileSuffix) {
75118
throw messages.createError('missingMetaFileSuffix', [this.type.name]);
@@ -81,6 +124,9 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter {
81124
if (this.isBundleType()) {
82125
return path;
83126
}
127+
if (isWebAppBaseType(path)) {
128+
return path;
129+
}
84130
const pathToContent = dirname(path);
85131
const parts = pathToContent.split(sep);
86132
/* Handle mobile or tablet variants.Eg- digitalExperiences/site/lwr11/sfdc_cms__view/home/mobile/mobile.json
@@ -104,6 +150,9 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter {
104150
// for top level types we don't need to resolve parent
105151
return component;
106152
}
153+
if (isWebAppBaseType(trigger)) {
154+
return this.populateWebAppBundle(trigger, component);
155+
}
107156
const source = super.populate(trigger, component);
108157
const parentType = this.registry.getParentType(this.type.id);
109158
// we expect source, parentType and content to be defined.
@@ -142,9 +191,45 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter {
142191
path: xml.path,
143192
};
144193
}
194+
if (xml && isWebAppBaseType(path)) {
195+
return {
196+
fullName: this.getBundleName(path),
197+
suffix: xml.suffix,
198+
path: xml.path,
199+
};
200+
}
201+
}
202+
203+
private populateWebAppBundle(trigger: string, component?: SourceComponent): SourceComponent {
204+
if (component) {
205+
return component;
206+
}
207+
const bundleName = this.getBundleName(trigger);
208+
const pathParts = trigger.split(sep);
209+
const bundleDir = pathParts.slice(0, getDigitalExperiencesIndex(trigger) + 3).join(sep);
210+
const parentType = this.isBundleType() ? this.type : this.registry.getParentType(this.type.id);
211+
if (!parentType) {
212+
throw messages.createError('error_failed_convert', [bundleName]);
213+
}
214+
return new SourceComponent(
215+
{
216+
name: bundleName,
217+
type: parentType,
218+
content: bundleDir,
219+
},
220+
this.tree,
221+
this.forceIgnore
222+
);
145223
}
146224

147225
private getBundleName(contentPath: string): string {
226+
if (isWebAppBaseType(contentPath)) {
227+
const pathParts = contentPath.split(sep);
228+
const digitalExperiencesIndex = getDigitalExperiencesIndex(contentPath);
229+
const baseType = pathParts[digitalExperiencesIndex + 1];
230+
const spaceApiName = pathParts[digitalExperiencesIndex + 2];
231+
return `${baseType}/${spaceApiName}`;
232+
}
148233
const bundlePath = this.getBundleMetadataXmlPath(contentPath);
149234
return `${parentName(dirname(bundlePath))}/${parentName(bundlePath)}`;
150235
}
@@ -154,6 +239,9 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter {
154239
// if this is the bundle type and it ends with -meta.xml, then this is the bundle metadata xml path
155240
return path;
156241
}
242+
if (isWebAppBaseType(path)) {
243+
return '';
244+
}
157245
const pathParts = path.split(sep);
158246
const typeFolderIndex = pathParts.lastIndexOf(this.type.directoryName);
159247
// 3 because we want 'digitalExperiences' directory, 'baseType' directory and 'bundleName' directory
@@ -177,3 +265,28 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter {
177265
const calculateNameFromPath = (contentPath: string): string => `${parentName(contentPath)}/${baseName(contentPath)}`;
178266
const digitalExperienceStructure = join('BaseType', 'SpaceApiName', 'ContentType', 'ContentApiName');
179267
const contentParts = digitalExperienceStructure.split(sep);
268+
269+
/**
270+
* Checks if the given path belongs to the web_app base type.
271+
* web_app base type has a simpler structure without ContentType folders.
272+
* Structure: digitalExperiences/web_app/spaceApiName/...files...
273+
*/
274+
const isWebAppBaseType = (path: string): boolean => {
275+
const pathParts = path.split(sep);
276+
const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences');
277+
// Check if the base type (folder after digitalExperiences) is WEB_APP_BASE_TYPE
278+
return (
279+
digitalExperiencesIndex > -1 &&
280+
pathParts.length > digitalExperiencesIndex + 1 &&
281+
pathParts[digitalExperiencesIndex + 1] === WEB_APP_BASE_TYPE
282+
);
283+
};
284+
285+
/**
286+
* Gets the digitalExperiences index from a path.
287+
* Returns -1 if not found.
288+
*/
289+
const getDigitalExperiencesIndex = (path: string): number => {
290+
const pathParts = path.split(sep);
291+
return pathParts.indexOf('digitalExperiences');
292+
};

src/resolve/metadataResolver.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ import { NodeFSTreeContainer, TreeContainer } from './treeContainers';
3131
Messages.importMessagesDirectory(__dirname);
3232
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
3333

34+
/**
35+
* Checks if the given path is a web_app bundle directory.
36+
* web_app bundles don't have metadata XML files and are identified by directory structure.
37+
*/
38+
const isWebAppBundlePath = (path: string): boolean => {
39+
const pathParts = path.split(sep);
40+
const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences');
41+
return (
42+
digitalExperiencesIndex > -1 &&
43+
pathParts.length > digitalExperiencesIndex + 2 &&
44+
pathParts[digitalExperiencesIndex + 1] === 'web_app'
45+
);
46+
};
47+
3448
/**
3549
* Resolver for metadata type and component objects.
3650
*
@@ -224,6 +238,10 @@ const resolveDirectoryAsComponent =
224238
(registry: RegistryAccess) =>
225239
(tree: TreeContainer) =>
226240
(dirPath: string): boolean => {
241+
if (isWebAppBundlePath(dirPath)) {
242+
return true;
243+
}
244+
227245
const type = resolveType(registry)(tree)(dirPath);
228246
if (type) {
229247
const { directoryName, inFolder } = type;
@@ -335,6 +353,10 @@ const resolveType =
335353
(registry: RegistryAccess) =>
336354
(tree: TreeContainer) =>
337355
(fsPath: string): MetadataType | undefined => {
356+
if (isWebAppBundlePath(fsPath)) {
357+
return registry.getTypeByName('DigitalExperienceBundle');
358+
}
359+
338360
// attempt 1 - check if the file is part of a component that requires a strict type folder
339361
let resolvedType = resolveTypeFromStrictFolder(registry)(fsPath);
340362

test/client/metadataApiDeploy.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,57 @@ describe('MetadataApiDeploy', () => {
12181218
expect(responses).to.deep.equal(expected);
12191219
});
12201220

1221+
it('should return FileResponses for web_app DigitalExperienceBundle with child files', () => {
1222+
const bundlePath = join('path', 'to', 'digitalExperiences', 'web_app', 'zenith');
1223+
const props = {
1224+
name: 'web_app/zenith',
1225+
type: registry.types.digitalexperiencebundle,
1226+
content: bundlePath,
1227+
};
1228+
const component = SourceComponent.createVirtualComponent(props, [
1229+
{
1230+
dirPath: bundlePath,
1231+
children: ['index.html', 'app.js', 'style.css'],
1232+
},
1233+
]);
1234+
const deployedSet = new ComponentSet([component]);
1235+
const { fullName, type } = component;
1236+
const apiStatus: Partial<MetadataApiDeployStatus> = {
1237+
details: {
1238+
componentSuccesses: {
1239+
changed: 'false',
1240+
created: 'true',
1241+
deleted: 'false',
1242+
success: 'true',
1243+
fullName,
1244+
componentType: type.name,
1245+
} as DeployMessage,
1246+
},
1247+
};
1248+
const result = new DeployResult(apiStatus as MetadataApiDeployStatus, deployedSet);
1249+
1250+
const responses = result.getFileResponses();
1251+
1252+
// Should have 1 bundle response + 3 file responses
1253+
expect(responses).to.have.lengthOf(4);
1254+
1255+
// First response should be the bundle
1256+
expect(responses[0]).to.deep.include({
1257+
fullName: 'web_app/zenith',
1258+
type: 'DigitalExperienceBundle',
1259+
state: ComponentStatus.Created,
1260+
filePath: bundlePath,
1261+
});
1262+
1263+
// Remaining responses should be DigitalExperience child files
1264+
const childResponses = responses.slice(1);
1265+
childResponses.forEach((response) => {
1266+
expect(response.type).to.equal('DigitalExperience');
1267+
expect(response.fullName).to.match(/^web_app\/zenith\//);
1268+
expect(response.state).to.equal(ComponentStatus.Created);
1269+
});
1270+
});
1271+
12211272
it('should cache fileResponses', () => {
12221273
const component = COMPONENT;
12231274
const deployedSet = new ComponentSet([component]);

test/resolve/adapters/digitalExperienceSourceAdapter.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,4 +192,26 @@ describe('DigitalExperienceSourceAdapter', () => {
192192
});
193193
});
194194
});
195+
196+
describe('DigitalExperienceSourceAdapter for web_app base type', () => {
197+
const WEBAPP_BUNDLE_PATH = join(BASE_PATH, 'web_app', 'zenith');
198+
const WEBAPP_CSS_FILE = join(WEBAPP_BUNDLE_PATH, 'css', 'home.css');
199+
200+
const webappTree = VirtualTreeContainer.fromFilePaths([WEBAPP_CSS_FILE]);
201+
202+
const webappBundleAdapter = new DigitalExperienceSourceAdapter(
203+
registry.types.digitalexperiencebundle,
204+
registryAccess,
205+
forceIgnore,
206+
webappTree
207+
);
208+
209+
it('should return a SourceComponent for web_app bundle directory (no meta.xml required)', () => {
210+
const component = webappBundleAdapter.getComponent(WEBAPP_BUNDLE_PATH);
211+
expect(component).to.not.be.undefined;
212+
expect(component?.type.name).to.equal('DigitalExperienceBundle');
213+
expect(component?.fullName).to.equal('web_app/zenith');
214+
expect(component?.content).to.equal(WEBAPP_BUNDLE_PATH);
215+
});
216+
});
195217
});

test/resolve/metadataResolver.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,22 @@ describe('MetadataResolver', () => {
266266
expect(mdResolver.getComponentsFromPath(path)).to.deep.equal([expectedComponent]);
267267
});
268268

269+
it('Should determine type for web_app DigitalExperienceBundle (no meta.xml required)', () => {
270+
const bundlePath = join('unpackaged', 'digitalExperiences', 'web_app', 'zenith');
271+
const filePath = join(bundlePath, 'index.html');
272+
const treeContainer = VirtualTreeContainer.fromFilePaths([filePath]);
273+
const mdResolver = new MetadataResolver(undefined, treeContainer);
274+
const expectedComponent = new SourceComponent(
275+
{
276+
name: 'web_app/zenith',
277+
type: registry.types.digitalexperiencebundle,
278+
content: bundlePath,
279+
},
280+
treeContainer
281+
);
282+
expect(mdResolver.getComponentsFromPath(bundlePath)).to.deep.equal([expectedComponent]);
283+
});
284+
269285
it('Should determine type for path of mixed content type', () => {
270286
const path = mixedContentDirectory.MIXED_CONTENT_DIRECTORY_SOURCE_PATHS[1];
271287
const access = testUtil.createMetadataResolver([

0 commit comments

Comments
 (0)