Skip to content

Commit f350cd6

Browse files
authored
feat: web compatibility W-20175875 (#1650)
* fix: deployResponses for non-set cwd * feat: deploy has FileResponses relative to projectDir if one exists on the CS and doesn't match cwd * refactor: isWebAppBundle as shared type guard * chore: empty * refactor: webappbundle also relative to project path if provided * chore: test failure on fullname fn * chore: accidentally removed code * test: ut for projectDir on CS
1 parent 036a989 commit f350cd6

File tree

9 files changed

+1417
-2853
lines changed

9 files changed

+1417
-2853
lines changed

CHANGELOG.md

Lines changed: 517 additions & 2024 deletions
Large diffs are not rendered by default.

METADATA_SUPPORT.md

Lines changed: 766 additions & 768 deletions
Large diffs are not rendered by default.

src/client/deployMessages.ts

Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
import { basename, dirname, extname, join, posix, sep } from 'node:path';
1818
import { SfError } from '@salesforce/core/sfError';
1919
import { ensureArray } from '@salesforce/kit';
20-
import { ComponentLike, SourceComponent } from '../resolve';
20+
import { SourceComponentWithContent, SourceComponent } from '../resolve/sourceComponent';
21+
import { ComponentLike } from '../resolve';
2122
import { registry } from '../registry/registry';
2223
import {
2324
BooleanString,
@@ -29,6 +30,7 @@ import {
2930
MetadataApiDeployStatus,
3031
} from './types';
3132
import { parseDeployDiagnostic } from './diagnosticUtil';
33+
import { isWebAppBundle } from './utils';
3234

3335
type DeployMessageWithComponentType = DeployMessage & { componentType: string };
3436
/**
@@ -79,50 +81,57 @@ const shouldWalkContent = (component: SourceComponent): boolean =>
7981
(t) => t.unaddressableWithoutParent === true || t.isAddressable === false
8082
));
8183

82-
export const createResponses = (component: SourceComponent, responseMessages: DeployMessage[]): FileResponse[] =>
83-
responseMessages.flatMap((message): FileResponse[] => {
84-
const state = getState(message);
85-
const base = { fullName: component.fullName, type: component.type.name } as const;
86-
87-
if (state === ComponentStatus.Failed) {
88-
return [{ ...base, state, ...parseDeployDiagnostic(component, message) } satisfies FileResponseFailure];
89-
} else {
90-
const isWebAppBundle =
91-
component.type.name === 'DigitalExperienceBundle' &&
92-
component.fullName.startsWith('web_app/') &&
93-
component.content;
94-
95-
if (isWebAppBundle) {
96-
const walkedPaths = component.walkContent();
97-
const bundleResponse: FileResponseSuccess = {
98-
fullName: component.fullName,
99-
type: component.type.name,
100-
state,
101-
filePath: component.content!,
102-
};
103-
const fileResponses: FileResponseSuccess[] = walkedPaths.map((filePath) => {
104-
// Normalize paths to ensure relative() works correctly on Windows
105-
const normalizedContent = component.content!.split(sep).join(posix.sep);
106-
const normalizedFilePath = filePath.split(sep).join(posix.sep);
107-
const relPath = posix.relative(normalizedContent, normalizedFilePath);
108-
return {
109-
fullName: posix.join(component.fullName, relPath),
110-
type: 'DigitalExperience',
111-
state,
112-
filePath,
113-
};
114-
});
115-
return [bundleResponse, ...fileResponses];
84+
export const createResponses =
85+
(projectPath?: string) =>
86+
(component: SourceComponent, responseMessages: DeployMessage[]): FileResponse[] =>
87+
responseMessages.flatMap((message): FileResponse[] => {
88+
const state = getState(message);
89+
const base = { fullName: component.fullName, type: component.type.name } as const;
90+
91+
if (state === ComponentStatus.Failed) {
92+
return [{ ...base, state, ...parseDeployDiagnostic(component, message) } satisfies FileResponseFailure];
11693
}
11794

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

src/client/metadataApiDeploy.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,11 +509,14 @@ const buildFileResponsesFromComponentSet =
509509

510510
const fileResponses = (cs.getSourceComponents().toArray() ?? [])
511511
.flatMap((deployedComponent) =>
512-
createResponses(deployedComponent, responseMessages.get(toKey(deployedComponent)) ?? []).concat(
512+
createResponses(cs.projectDirectory)(
513+
deployedComponent,
514+
responseMessages.get(toKey(deployedComponent)) ?? []
515+
).concat(
513516
deployedComponent.type.children
514517
? deployedComponent.getChildren().flatMap((child) => {
515518
const childMessages = responseMessages.get(toKey(child));
516-
return childMessages ? createResponses(child, childMessages) : [];
519+
return childMessages ? createResponses(cs.projectDirectory)(child, childMessages) : [];
517520
})
518521
: []
519522
)

src/client/metadataApiRetrieve.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
import { extract } from './retrieveExtract';
3939
import { getPackageOptions } from './retrieveExtract';
4040
import { MetadataApiRetrieveOptions } from './types';
41+
import { isWebAppBundle } from './utils';
4142

4243
Messages.importMessagesDirectory(__dirname);
4344
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
@@ -101,28 +102,27 @@ export class RetrieveResult implements MetadataTransferResult {
101102

102103
// construct successes
103104
for (const retrievedComponent of this.components.getSourceComponents()) {
104-
const { fullName, type, xml, content } = retrievedComponent;
105+
const { fullName, type, xml } = retrievedComponent;
105106
const baseResponse = {
106107
fullName,
107108
type: type.name,
108109
state: this.localComponents.has(retrievedComponent) ? ComponentStatus.Changed : ComponentStatus.Created,
109110
} as const;
110111

111112
// 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();
113+
if (isWebAppBundle(retrievedComponent)) {
116114
// 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-
}
115+
this.fileResponses.push(
116+
...[retrievedComponent.content, ...retrievedComponent.walkContent()].map(
117+
(filePath) => ({ ...baseResponse, filePath } satisfies FileResponseSuccess)
118+
)
119+
);
122120
} else if (!type.children || Object.values(type.children.types).some((t) => t.unaddressableWithoutParent)) {
123-
for (const filePath of retrievedComponent.walkContent()) {
124-
this.fileResponses.push({ ...baseResponse, filePath } satisfies FileResponseSuccess);
125-
}
121+
this.fileResponses.push(
122+
...retrievedComponent
123+
.walkContent()
124+
.map((filePath) => ({ ...baseResponse, filePath } satisfies FileResponseSuccess))
125+
);
126126
}
127127

128128
if (xml) {

src/client/retrieveExtract.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { ConvertOutputConfig } from '../convert/types';
2121
import { MetadataConverter } from '../convert/metadataConverter';
2222
import { ComponentSet } from '../collections/componentSet';
2323
import { ZipTreeContainer } from '../resolve/treeContainers';
24-
import { SourceComponent } from '../resolve/sourceComponent';
24+
import { SourceComponent, SourceComponentWithContent } from '../resolve/sourceComponent';
2525
import { fnJoin } from '../utils/path';
2626
import { ComponentStatus, FileResponse, FileResponseSuccess, PackageOption, PackageOptions } from './types';
2727
import { MetadataApiRetrieveOptions } from './types';
@@ -147,12 +147,12 @@ const handlePartialDeleteMerges = ({
147147
});
148148
};
149149

150-
const supportsPartialDeleteAndHasContent = (comp: SourceComponent): comp is SourceComponent & { content: string } =>
150+
const supportsPartialDeleteAndHasContent = (comp: SourceComponent): comp is SourceComponentWithContent =>
151151
supportsPartialDelete(comp) && typeof comp.content === 'string' && fs.statSync(comp.content).isDirectory();
152152

153153
const supportsPartialDeleteAndHasZipContent =
154154
(tree: ZipTreeContainer) =>
155-
(comp: SourceComponent): comp is SourceComponent & { content: string } =>
155+
(comp: SourceComponent): comp is SourceComponentWithContent =>
156156
supportsPartialDelete(comp) && typeof comp.content === 'string' && tree.isDirectory(comp.content);
157157

158158
const supportsPartialDeleteAndIsInMap =

src/client/utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2025, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { SourceComponent, SourceComponentWithContent } from '../resolve/sourceComponent';
17+
18+
export const isWebAppBundle = (component: SourceComponent): component is SourceComponentWithContent =>
19+
component.type.name === 'DigitalExperienceBundle' &&
20+
component.fullName.startsWith('web_app/') &&
21+
typeof component.content === 'string';

src/resolve/sourceComponent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export type ComponentProperties = {
4747
parentType?: MetadataType;
4848
};
4949

50+
export type SourceComponentWithContent = SourceComponent & { content: string };
51+
5052
/**
5153
* Representation of a MetadataComponent in a file tree.
5254
*/

test/client/metadataApiDeploy.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,44 @@ describe('MetadataApiDeploy', () => {
12931293
result.getFileResponses();
12941294
expect(spy.callCount).to.equal(1);
12951295
});
1296+
1297+
it('should prepend projectDirectory to filePaths when projectDirectory differs from cwd', () => {
1298+
const component = matchingContentFile.COMPONENT;
1299+
const projectDir = join('my', 'project', 'dir');
1300+
const deployedSet = new ComponentSet([component]);
1301+
deployedSet.projectDirectory = projectDir;
1302+
const { fullName, type, content, xml } = component;
1303+
const apiStatus: Partial<MetadataApiDeployStatus> = {
1304+
details: {
1305+
componentSuccesses: {
1306+
changed: 'true',
1307+
created: 'false',
1308+
deleted: 'false',
1309+
fullName,
1310+
componentType: type.name,
1311+
} as DeployMessage,
1312+
},
1313+
};
1314+
const result = new DeployResult(apiStatus as MetadataApiDeployStatus, deployedSet);
1315+
1316+
const responses = result.getFileResponses();
1317+
const expected: FileResponse[] = [
1318+
{
1319+
fullName,
1320+
type: type.name,
1321+
state: ComponentStatus.Changed,
1322+
filePath: join(projectDir, ensureString(content)),
1323+
},
1324+
{
1325+
fullName,
1326+
type: type.name,
1327+
state: ComponentStatus.Changed,
1328+
filePath: join(projectDir, ensureString(xml)),
1329+
},
1330+
];
1331+
1332+
expect(responses).to.deep.equal(expected);
1333+
});
12961334
});
12971335
});
12981336

0 commit comments

Comments
 (0)