Skip to content

Commit b0a1895

Browse files
Shinonilesya7
andauthored
@W-20799435 feat: add WebApplication metadata type (#1672)
* feat: add WebApplication metadata type * chore: remove stray comment * fix: treat webApplications as bundle * chore: remove converter comment * test: add webapplication snapshots * test: update webapplication snapshots * test: allow extra resolution files for bundle --------- Co-authored-by: shinoni <lturanscaia@salesforce.com>
1 parent a64d204 commit b0a1895

33 files changed

Lines changed: 579 additions & 7 deletions

File tree

src/registry/metadataRegistry.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
"staticresources": "staticresource",
7272
"waveTemplates": "wavetemplatebundle",
7373
"appTemplates": "appframeworktemplatebundle",
74-
"lightningTypes": "lightningtypebundle"
74+
"lightningTypes": "lightningtypebundle",
75+
"webapplications": "webapplication"
7576
},
7677
"suffixes": {
7778
"Canvas": "canvasmetadata",
@@ -527,6 +528,7 @@
527528
"wds": "wavedataset",
528529
"webStoreBundle": "webstorebundle",
529530
"webStoreTemplate": "webstoretemplate",
531+
"webapplication": "webapplication",
530532
"workflowFlowAutomation": "workflowflowautomation",
531533
"weblink": "custompageweblink",
532534
"wlens": "wavelens",
@@ -4670,6 +4672,18 @@
46704672
"name": "WaveXmd",
46714673
"suffix": "xmd"
46724674
},
4675+
"webapplication": {
4676+
"directoryName": "webapplications",
4677+
"id": "webapplication",
4678+
"inFolder": false,
4679+
"name": "WebApplication",
4680+
"suffix": "webapplication",
4681+
"strategies": {
4682+
"adapter": "webApplications"
4683+
},
4684+
"strictDirectoryName": true,
4685+
"supportsPartialDelete": true
4686+
},
46734687
"webstorebundle": {
46744688
"directoryName": "webStoreBundles",
46754689
"id": "webstorebundle",

src/registry/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ export type MetadataType = {
155155
| 'digitalExperience'
156156
| 'bundle'
157157
| 'default'
158-
| 'partiallyDecomposed';
158+
| 'partiallyDecomposed'
159+
| 'webApplications';
159160
transformer?:
160161
| 'decomposed'
161162
| 'staticResource'

src/resolve/adapters/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export { DecomposedSourceAdapter } from './decomposedSourceAdapter';
2020
export { DefaultSourceAdapter } from './defaultSourceAdapter';
2121
export { BaseSourceAdapter } from './baseSourceAdapter';
2222
export { DigitalExperienceSourceAdapter } from './digitalExperienceSourceAdapter';
23+
export { WebApplicationsSourceAdapter } from './webApplicationsSourceAdapter';

src/resolve/adapters/sourceAdapterFactory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { MatchingContentSourceAdapter } from './matchingContentSourceAdapter';
2626
import { MixedContentSourceAdapter } from './mixedContentSourceAdapter';
2727
import { DefaultSourceAdapter } from './defaultSourceAdapter';
2828
import { DigitalExperienceSourceAdapter } from './digitalExperienceSourceAdapter';
29+
import { WebApplicationsSourceAdapter } from './webApplicationsSourceAdapter';
2930
import { PartialDecomposedAdapter } from './partialDecomposedAdapter';
3031

3132
Messages.importMessagesDirectory(__dirname);
@@ -53,6 +54,8 @@ export class SourceAdapterFactory {
5354
return new MixedContentSourceAdapter(type, this.registry, forceIgnore, this.tree);
5455
case 'digitalExperience':
5556
return new DigitalExperienceSourceAdapter(type, this.registry, forceIgnore, this.tree);
57+
case 'webApplications':
58+
return new WebApplicationsSourceAdapter(type, this.registry, forceIgnore, this.tree);
5659
case 'partiallyDecomposed':
5760
return new PartialDecomposedAdapter(type, this.registry, forceIgnore, this.tree);
5861
case 'default':
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2026, 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 { join } from 'node:path';
17+
import { Messages } from '@salesforce/core/messages';
18+
import { SfError } from '@salesforce/core/sfError';
19+
import { SourcePath } from '../../common/types';
20+
import { SourceComponent } from '../sourceComponent';
21+
import { baseName } from '../../utils/path';
22+
import { BundleSourceAdapter } from './bundleSourceAdapter';
23+
24+
Messages.importMessagesDirectory(__dirname);
25+
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
26+
27+
export class WebApplicationsSourceAdapter extends BundleSourceAdapter {
28+
// Enforces WebApplication bundle requirements for source/deploy while staying
29+
// compatible with metadata-only retrievals.
30+
protected populate(
31+
trigger: SourcePath,
32+
component?: SourceComponent,
33+
isResolvingSource = true
34+
): SourceComponent | undefined {
35+
const source = super.populate(trigger, component);
36+
if (!source?.content) {
37+
return source;
38+
}
39+
40+
const contentPath = source.content;
41+
const appName = baseName(contentPath);
42+
const expectedXmlPath = join(contentPath, `${appName}.webapplication-meta.xml`);
43+
if (!this.tree.exists(expectedXmlPath)) {
44+
throw new SfError(
45+
messages.getMessage('error_expected_source_files', [expectedXmlPath, this.type.name]),
46+
'ExpectedSourceFilesError'
47+
);
48+
}
49+
50+
const resolvedSource =
51+
source.xml && source.xml === expectedXmlPath
52+
? source
53+
: new SourceComponent(
54+
{
55+
name: source.name,
56+
type: source.type,
57+
content: source.content,
58+
xml: expectedXmlPath,
59+
parent: source.parent,
60+
parentType: source.parentType,
61+
},
62+
this.tree,
63+
this.forceIgnore
64+
);
65+
66+
if (isResolvingSource) {
67+
const descriptorPath = join(contentPath, 'webapplication.json');
68+
const xmlFileName = `${appName}.webapplication-meta.xml`;
69+
const contentEntries = (this.tree.readDirectory(contentPath) ?? []).filter(
70+
(entry) => entry !== xmlFileName && entry !== 'webapplication.json'
71+
);
72+
if (contentEntries.length === 0) {
73+
// For deploy/source, we expect at least one non-metadata content file (e.g. index.html).
74+
throw new SfError(
75+
messages.getMessage('error_expected_source_files', [contentPath, this.type.name]),
76+
'ExpectedSourceFilesError'
77+
);
78+
}
79+
if (!this.tree.exists(descriptorPath)) {
80+
throw new SfError(
81+
messages.getMessage('error_expected_source_files', [descriptorPath, this.type.name]),
82+
'ExpectedSourceFilesError'
83+
);
84+
}
85+
if (this.forceIgnore.denies(descriptorPath)) {
86+
throw messages.createError('noSourceIgnore', [this.type.name, descriptorPath]);
87+
}
88+
}
89+
90+
return resolvedSource;
91+
}
92+
}

src/resolve/metadataResolver.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,8 @@ const legacySuffixMatches = (type: MetadataType, fsPath: string): boolean => {
458458
const appendMetaXmlSuffix = (suffix: string): string => `${suffix}${META_XML_SUFFIX}`;
459459

460460
const isMixedContentOrBundle = (type: MetadataType): boolean =>
461-
typeof type.strategies?.adapter === 'string' && ['mixedContent', 'bundle'].includes(type.strategies.adapter);
461+
typeof type.strategies?.adapter === 'string' &&
462+
['mixedContent', 'bundle', 'webApplications'].includes(type.strategies.adapter);
462463

463464
/** types with folders only have folder components living at the top level.
464465
* if the fsPath is a folder component, let a future strategy deal with it

src/utils/filePathGenerator.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,20 @@ export const filePathsFromMetadataComponent = (
119119
];
120120
}
121121

122-
// lwc, aura, waveTemplate, experiencePropertyType, lightningTypeBundle, contentTypeBundle
123-
if (type.strategies?.adapter === 'bundle') {
122+
// lwc, aura, waveTemplate, experiencePropertyType, lightningTypeBundle, contentTypeBundle, webApplications
123+
if (type.strategies?.adapter === 'bundle' || type.strategies?.adapter === 'webApplications') {
124124
const mappings = new Map<string, string[]>([
125125
['ExperiencePropertyTypeBundle', [join(packageDirWithTypeDir, `${fullName}${sep}schema.json`)]],
126126
['LightningTypeBundle', [join(packageDirWithTypeDir, `${fullName}${sep}schema.json`)]],
127127
['ContentTypeBundle', [join(packageDirWithTypeDir, `${fullName}${sep}schema.json`)]],
128128
['WaveTemplateBundle', [join(packageDirWithTypeDir, `${fullName}${sep}template-info.json`)]],
129+
[
130+
'WebApplication',
131+
[
132+
join(packageDirWithTypeDir, `${fullName}${sep}webapplication.json`),
133+
join(packageDirWithTypeDir, `${fullName}${sep}${fullName}.webapplication${META_XML_SUFFIX}`),
134+
],
135+
],
129136
['LightningComponentBundle', [join(packageDirWithTypeDir, `${fullName}${sep}${fullName}.js${META_XML_SUFFIX}`)]],
130137
['AuraDefinitionBundle', [join(packageDirWithTypeDir, `${fullName}${sep}${fullName}.cmp${META_XML_SUFFIX}`)]],
131138
['GenAiFunction', [join(packageDirWithTypeDir, `${fullName}${sep}${fullName}.genAiFunction${META_XML_SUFFIX}`)]],

src/utils/path.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export const calculateRelativePath =
150150
!suffix ||
151151
Boolean(inFolder) ||
152152
typeof folderContentType === 'string' ||
153-
['digitalexperiencebundle', 'digitalexperience'].includes(types.self.id)
153+
['digitalexperiencebundle', 'digitalexperience', 'webapplication'].includes(types.self.id)
154154
) {
155155
return join(base, trimUntil(fsPath, directoryName, true));
156156
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<WebApplication xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<masterLabel>Happy App</masterLabel>
4+
<version>1.0</version>
5+
<isActive>true</isActive>
6+
</WebApplication>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Happy App</title>
6+
</head>
7+
<body>
8+
<h1>Happy App</h1>
9+
</body>
10+
</html>

0 commit comments

Comments
 (0)