Skip to content

Commit 5881c94

Browse files
committed
Merge remote-tracking branch 'origin/main' into sm/web-compatibility
2 parents 810a95a + 036a989 commit 5881c94

File tree

34 files changed

+2105
-2803
lines changed

34 files changed

+2105
-2803
lines changed

CHANGELOG.md

Lines changed: 523 additions & 2012 deletions
Large diffs are not rendered by default.

METADATA_SUPPORT.md

Lines changed: 767 additions & 765 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@salesforce/source-deploy-retrieve",
3-
"version": "12.27.1",
3+
"version": "12.28.0",
44
"description": "JavaScript library to run Salesforce metadata deploys and retrieves",
55
"main": "lib/src/index.js",
66
"author": "Salesforce",

src/client/deployMessages.ts

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { basename, dirname, extname, join, posix, sep } from 'node:path/posix';
17+
import { basename, dirname, extname, join, posix, sep } from 'node:path';
1818
import { SfError } from '@salesforce/core/sfError';
1919
import { ensureArray } from '@salesforce/kit';
2020
import { ComponentLike, SourceComponent } from '../resolve';
@@ -29,6 +29,7 @@ import {
2929
MetadataApiDeployStatus,
3030
} from './types';
3131
import { parseDeployDiagnostic } from './diagnosticUtil';
32+
import { isWebAppBundle } from './utils';
3233

3334
type DeployMessageWithComponentType = DeployMessage & { componentType: string };
3435
/**
@@ -88,27 +89,49 @@ export const createResponses =
8889

8990
if (state === ComponentStatus.Failed) {
9091
return [{ ...base, state, ...parseDeployDiagnostic(component, message) } satisfies FileResponseFailure];
91-
} else {
92-
return (
93-
[
94-
...(shouldWalkContent(component)
95-
? component.walkContent().map((filePath): FileResponseSuccess => ({ ...base, state, filePath }))
96-
: []),
97-
...(component.xml ? [{ ...base, state, filePath: component.xml } satisfies FileResponseSuccess] : []),
98-
]
99-
// deployResults will produce filePaths relative to cwd, which might not be set in all environments
100-
// if our CS had a projectDir set, we'll make the results relative to that path
101-
.map((response) => ({
102-
...response,
103-
filePath:
104-
projectPath && process.cwd() === projectPath
105-
? response.filePath
106-
: join(projectPath ?? '', response.filePath),
107-
}))
108-
);
10992
}
110-
});
11193

94+
if (isWebAppBundle(component)) {
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+
// Normalize paths to ensure relative() works correctly on Windows
104+
const normalizedContent = component.content.split(sep).join(posix.sep);
105+
const normalizedFilePath = filePath.split(sep).join(posix.sep);
106+
const relPath = posix.relative(normalizedContent, normalizedFilePath);
107+
return {
108+
fullName: posix.join(component.fullName, relPath),
109+
type: 'DigitalExperience',
110+
state,
111+
filePath,
112+
};
113+
});
114+
return [bundleResponse, ...fileResponses];
115+
}
116+
117+
return (
118+
[
119+
...(shouldWalkContent(component)
120+
? component.walkContent().map((filePath): FileResponseSuccess => ({ ...base, state, filePath }))
121+
: []),
122+
...(component.xml ? [{ ...base, state, filePath: component.xml } satisfies FileResponseSuccess] : []),
123+
]
124+
// deployResults will produce filePaths relative to cwd, which might not be set in all environments
125+
// if our CS had a projectDir set, we'll make the results relative to that path
126+
.map((response) => ({
127+
...response,
128+
filePath:
129+
projectPath && process.cwd() === projectPath
130+
? response.filePath
131+
: join(projectPath ?? '', response.filePath),
132+
}))
133+
);
134+
});
112135
/**
113136
* Groups messages from the deploy result by component fullName and type
114137
*/

src/client/metadataApiRetrieve.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,25 @@ export class RetrieveResult implements MetadataTransferResult {
101101

102102
// construct successes
103103
for (const retrievedComponent of this.components.getSourceComponents()) {
104-
const { fullName, type, xml } = retrievedComponent;
104+
const { fullName, type, xml, content } = retrievedComponent;
105105
const baseResponse = {
106106
fullName,
107107
type: type.name,
108108
state: this.localComponents.has(retrievedComponent) ? ComponentStatus.Changed : ComponentStatus.Created,
109109
} as const;
110110

111-
if (!type.children || Object.values(type.children.types).some((t) => t.unaddressableWithoutParent)) {
111+
// Special handling for web_app bundles - they need to walk content and report individual files
112+
const isWebAppBundle = type.name === 'DigitalExperienceBundle' && fullName.startsWith('web_app/') && content;
113+
114+
if (isWebAppBundle) {
115+
const walkedPaths = retrievedComponent.walkContent();
116+
// Add the bundle directory itself
117+
this.fileResponses.push({ ...baseResponse, filePath: content } satisfies FileResponseSuccess);
118+
// Add each file with its specific path
119+
for (const filePath of walkedPaths) {
120+
this.fileResponses.push({ ...baseResponse, filePath } satisfies FileResponseSuccess);
121+
}
122+
} else if (!type.children || Object.values(type.children.types).some((t) => t.unaddressableWithoutParent)) {
112123
for (const filePath of retrievedComponent.walkContent()) {
113124
this.fileResponses.push({ ...baseResponse, filePath } satisfies FileResponseSuccess);
114125
}

src/convert/transformers/defaultMetadataTransformer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ const getContentSourceDestination = (
8585
if (mergeWith?.content) {
8686
if (component.content && component.tree.isDirectory(component.content)) {
8787
// DEs are always inside a dir.
88-
if (component.type.strategies?.adapter === 'digitalExperience') {
88+
// For web_app base type, use standard relative path logic (no ContentType folders)
89+
const isWebApp = source.includes(`${sep}web_app${sep}`);
90+
if (component.type.strategies?.adapter === 'digitalExperience' && !isWebApp) {
8991
const parts = source.split(sep);
9092
const file = parts.pop() ?? '';
9193
const dir = join(mergeWith.content, parts.pop() ?? '');

src/registry/metadataRegistry.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@
567567
"integrationProcdDefinition": "integrationprocddefinition",
568568
"omniscriptDefinition": "omniscriptdefinition",
569569
"computeExtension": "computeextension",
570+
"dataObjectSearchIndexConf": "dataobjectsearchindexconf",
570571
"lightningOutApp": "lightningoutapp"
571572
},
572573
"types": {
@@ -5053,6 +5054,14 @@
50535054
"inFolder": false,
50545055
"strictDirectoryName": false
50555056
},
5057+
"dataobjectsearchindexconf": {
5058+
"id": "dataobjectsearchindexconf",
5059+
"name": "DataObjectSearchIndexConf",
5060+
"suffix": "dataObjectSearchIndexConf",
5061+
"directoryName": "dataObjectSearchIndexConfs",
5062+
"inFolder": false,
5063+
"strictDirectoryName": false
5064+
},
50565065
"lightningoutapp": {
50575066
"id": "lightningoutapp",
50585067
"name": "LightningOutApp",

src/resolve/adapters/digitalExperienceSourceAdapter.ts

Lines changed: 130 additions & 2 deletions
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,10 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter {
81124
if (this.isBundleType()) {
82125
return path;
83126
}
127+
if (isWebAppBaseType(path)) {
128+
// For web_app, trim to the bundle directory: digitalExperiences/web_app/WebApp
129+
return getWebAppBundleDir(path);
130+
}
84131
const pathToContent = dirname(path);
85132
const parts = pathToContent.split(sep);
86133
/* Handle mobile or tablet variants.Eg- digitalExperiences/site/lwr11/sfdc_cms__view/home/mobile/mobile.json
@@ -104,6 +151,9 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter {
104151
// for top level types we don't need to resolve parent
105152
return component;
106153
}
154+
if (isWebAppBaseType(trigger)) {
155+
return this.populateWebAppBundle(trigger, component);
156+
}
107157
const source = super.populate(trigger, component);
108158
const parentType = this.registry.getParentType(this.type.id);
109159
// we expect source, parentType and content to be defined.
@@ -144,16 +194,59 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter {
144194
}
145195
}
146196

197+
private populateWebAppBundle(trigger: string, component?: SourceComponent): SourceComponent {
198+
if (component) {
199+
return component;
200+
}
201+
202+
const pathParts = trigger.split(sep);
203+
const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences');
204+
205+
// Extract bundle name: web_app/WebApp3 (always use posix separator for metadata names)
206+
const baseType = pathParts[digitalExperiencesIndex + 1];
207+
const spaceApiName = pathParts[digitalExperiencesIndex + 2];
208+
const bundleName = [baseType, spaceApiName].join('/');
209+
210+
// Extract bundle directory: /path/to/digitalExperiences/web_app/WebApp3
211+
const bundleDir = getWebAppBundleDir(trigger);
212+
213+
// Get the DigitalExperienceBundle type
214+
const parentType = this.isBundleType() ? this.type : this.registry.getParentType(this.type.id);
215+
if (!parentType) {
216+
throw messages.createError('error_failed_convert', [bundleName]);
217+
}
218+
219+
return new SourceComponent(
220+
{
221+
name: bundleName,
222+
type: parentType,
223+
content: bundleDir,
224+
},
225+
this.tree,
226+
this.forceIgnore
227+
);
228+
}
229+
147230
private getBundleName(contentPath: string): string {
231+
if (isWebAppBaseType(contentPath)) {
232+
const pathParts = contentPath.split(sep);
233+
const digitalExperiencesIndex = getDigitalExperiencesIndex(contentPath);
234+
const baseType = pathParts[digitalExperiencesIndex + 1];
235+
const spaceApiName = pathParts[digitalExperiencesIndex + 2];
236+
return [baseType, spaceApiName].join('/');
237+
}
148238
const bundlePath = this.getBundleMetadataXmlPath(contentPath);
149-
return `${parentName(dirname(bundlePath))}/${parentName(bundlePath)}`;
239+
return [parentName(dirname(bundlePath)), parentName(bundlePath)].join('/');
150240
}
151241

152242
private getBundleMetadataXmlPath(path: string): string {
153243
if (this.isBundleType() && path.endsWith(META_XML_SUFFIX)) {
154244
// if this is the bundle type and it ends with -meta.xml, then this is the bundle metadata xml path
155245
return path;
156246
}
247+
if (isWebAppBaseType(path)) {
248+
return '';
249+
}
157250
const pathParts = path.split(sep);
158251
const typeFolderIndex = pathParts.lastIndexOf(this.type.directoryName);
159252
// 3 because we want 'digitalExperiences' directory, 'baseType' directory and 'bundleName' directory
@@ -177,3 +270,38 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter {
177270
const calculateNameFromPath = (contentPath: string): string => `${parentName(contentPath)}/${baseName(contentPath)}`;
178271
const digitalExperienceStructure = join('BaseType', 'SpaceApiName', 'ContentType', 'ContentApiName');
179272
const contentParts = digitalExperienceStructure.split(sep);
273+
274+
/**
275+
* Checks if the given path belongs to the web_app base type.
276+
* web_app base type has a simpler structure without ContentType folders.
277+
* Structure: digitalExperiences/web_app/spaceApiName/...files...
278+
*/
279+
export const isWebAppBaseType = (path: string): boolean => {
280+
const pathParts = path.split(sep);
281+
const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences');
282+
return pathParts[digitalExperiencesIndex + 1] === WEB_APP_BASE_TYPE;
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+
};
293+
294+
/**
295+
* Gets the web_app bundle directory path.
296+
* For a path like: /path/to/digitalExperiences/web_app/WebApp/src/App.js
297+
* Returns: /path/to/digitalExperiences/web_app/WebApp
298+
*/
299+
const getWebAppBundleDir = (path: string): string => {
300+
const pathParts = path.split(sep);
301+
const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences');
302+
if (digitalExperiencesIndex > -1 && pathParts.length > digitalExperiencesIndex + 3) {
303+
// Return up to digitalExperiences/web_app/spaceApiName
304+
return pathParts.slice(0, digitalExperiencesIndex + 3).join(sep);
305+
}
306+
return path;
307+
};

src/resolve/metadataResolver.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { SourceAdapterFactory } from './adapters/sourceAdapterFactory';
2727
import { ForceIgnore } from './forceIgnore';
2828
import { SourceComponent } from './sourceComponent';
2929
import { NodeFSTreeContainer, TreeContainer } from './treeContainers';
30+
import { isWebAppBaseType } from './adapters/digitalExperienceSourceAdapter';
3031

3132
Messages.importMessagesDirectory(__dirname);
3233
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
@@ -224,6 +225,15 @@ const resolveDirectoryAsComponent =
224225
(registry: RegistryAccess) =>
225226
(tree: TreeContainer) =>
226227
(dirPath: string): boolean => {
228+
// For web_app bundles, only the bundle directory itself should be resolved as a component
229+
// (e.g., digitalExperiences/web_app/WebApp), not subdirectories like src/, public/, etc.
230+
if (isWebAppBaseType(dirPath)) {
231+
const pathParts = dirPath.split(sep);
232+
const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences');
233+
// The bundle directory is exactly 3 levels deep: digitalExperiences/web_app/bundleName
234+
return digitalExperiencesIndex !== -1 && pathParts.length === digitalExperiencesIndex + 3;
235+
}
236+
227237
const type = resolveType(registry)(tree)(dirPath);
228238
if (type) {
229239
const { directoryName, inFolder } = type;
@@ -335,6 +345,10 @@ const resolveType =
335345
(registry: RegistryAccess) =>
336346
(tree: TreeContainer) =>
337347
(fsPath: string): MetadataType | undefined => {
348+
if (isWebAppBaseType(fsPath)) {
349+
return registry.getTypeByName('DigitalExperienceBundle');
350+
}
351+
338352
// attempt 1 - check if the file is part of a component that requires a strict type folder
339353
let resolvedType = resolveTypeFromStrictFolder(registry)(fsPath);
340354

0 commit comments

Comments
 (0)