From dd0eddbf5e789c22a6e761aea36c81729b21e839 Mon Sep 17 00:00:00 2001 From: pyelchuri Date: Mon, 10 Nov 2025 10:50:03 +0530 Subject: [PATCH 1/9] feat: Support web app bundle @W-20091482 --- src/client/deployMessages.ts | 24 ++++ .../digitalExperienceSourceAdapter.ts | 115 +++++++++++++++++- src/resolve/metadataResolver.ts | 22 ++++ test/client/metadataApiDeploy.test.ts | 51 ++++++++ .../digitalExperienceSourceAdapter.test.ts | 22 ++++ test/resolve/metadataResolver.test.ts | 16 +++ 6 files changed, 249 insertions(+), 1 deletion(-) diff --git a/src/client/deployMessages.ts b/src/client/deployMessages.ts index d7675b35f..b60d27c50 100644 --- a/src/client/deployMessages.ts +++ b/src/client/deployMessages.ts @@ -87,6 +87,30 @@ export const createResponses = (component: SourceComponent, responseMessages: De if (state === ComponentStatus.Failed) { return [{ ...base, state, ...parseDeployDiagnostic(component, message) } satisfies FileResponseFailure]; } else { + const isWebAppBundle = component.type.name === 'DigitalExperienceBundle' && + component.fullName.startsWith('web_app/') && + component.content; + + if (isWebAppBundle) { + const walkedPaths = component.walkContent(); + const bundleResponse: FileResponseSuccess = { + fullName: component.fullName, + type: component.type.name, + state, + filePath: component.content!, + }; + const fileResponses: FileResponseSuccess[] = walkedPaths.map((filePath) => { + const relPath = filePath.replace(component.content! + '/', ''); + return { + fullName: `${component.fullName}/${relPath}`, + type: 'DigitalExperience', + state, + filePath, + }; + }); + return [bundleResponse, ...fileResponses]; + } + return [ ...(shouldWalkContent(component) ? component.walkContent().map((filePath): FileResponseSuccess => ({ ...base, state, filePath })) diff --git a/src/resolve/adapters/digitalExperienceSourceAdapter.ts b/src/resolve/adapters/digitalExperienceSourceAdapter.ts index 1a0bf2f46..75cb07b50 100644 --- a/src/resolve/adapters/digitalExperienceSourceAdapter.ts +++ b/src/resolve/adapters/digitalExperienceSourceAdapter.ts @@ -25,6 +25,10 @@ import { BundleSourceAdapter } from './bundleSourceAdapter'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); + +// Constants for DigitalExperience base types +const WEB_APP_BASE_TYPE = 'web_app'; + /** * Source Adapter for DigitalExperience metadata types. This metadata type is a bundled type of the format * @@ -58,18 +62,57 @@ const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sd * content/ * ├── bars/ * | ├── bars.digitalExperience-meta.xml + * web_app/ + * ├── zenith/ + * | ├── css/ + * | | ├── header/ + * | | | ├── header.css + * | | ├── home.css + * | ├── js/ + * | | ├── home.js + * | ├── html/ + * | | ├── home.html + * | ├── images/ + * | | ├── logos/ + * | | | ├── logo.png * ``` * * In the above structure the metadata xml file ending with "digitalExperience-meta.xml" belongs to DigitalExperienceBundle MD type. * The "_meta.json" files are child metadata files of DigitalExperienceBundle belonging to DigitalExperience MD type. The rest of the files in the * corresponding folder are the contents to the DigitalExperience metadata. So, incase of DigitalExperience the metadata file is a JSON file - * and not an XML file + * and not an XML file. + * + * For web_app base type, the bundle is identified by directory structure alone without metadata XML files. */ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter { + public getComponent(path: SourcePath, isResolvingSource = true): SourceComponent | undefined { + if (this.isBundleType() && isWebAppBaseType(path) && this.tree.isDirectory(path)) { + const pathParts = path.split(sep); + const bundleNameIndex = getDigitalExperiencesIndex(path) + 2; + if (bundleNameIndex === pathParts.length - 1) { + return this.populate(path, undefined); + } + } + return super.getComponent(path, isResolvingSource); + } + + protected parseAsRootMetadataXml(path: string): MetadataXml | undefined { + if (isWebAppBaseType(path)) { + return undefined; + } + if (!this.isBundleType() && !path.endsWith(this.type.metaFileSuffix ?? '_meta.json')) { + return undefined; + } + return super.parseAsRootMetadataXml(path); + } + protected getRootMetadataXmlPath(trigger: string): string { if (this.isBundleType()) { return this.getBundleMetadataXmlPath(trigger); } + if (isWebAppBaseType(trigger)) { + return ''; + } // metafile name = metaFileSuffix for DigitalExperience. if (!this.type.metaFileSuffix) { throw messages.createError('missingMetaFileSuffix', [this.type.name]); @@ -81,6 +124,9 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter { if (this.isBundleType()) { return path; } + if (isWebAppBaseType(path)) { + return path; + } const pathToContent = dirname(path); const parts = pathToContent.split(sep); /* 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 { // for top level types we don't need to resolve parent return component; } + if (isWebAppBaseType(trigger)) { + return this.populateWebAppBundle(trigger, component); + } const source = super.populate(trigger, component); const parentType = this.registry.getParentType(this.type.id); // we expect source, parentType and content to be defined. @@ -142,9 +191,45 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter { path: xml.path, }; } + if (xml && isWebAppBaseType(path)) { + return { + fullName: this.getBundleName(path), + suffix: xml.suffix, + path: xml.path, + }; + } + } + + private populateWebAppBundle(trigger: string, component?: SourceComponent): SourceComponent { + if (component) { + return component; + } + const bundleName = this.getBundleName(trigger); + const pathParts = trigger.split(sep); + const bundleDir = pathParts.slice(0, getDigitalExperiencesIndex(trigger) + 3).join(sep); + const parentType = this.isBundleType() ? this.type : this.registry.getParentType(this.type.id); + if (!parentType) { + throw messages.createError('error_failed_convert', [bundleName]); + } + return new SourceComponent( + { + name: bundleName, + type: parentType, + content: bundleDir, + }, + this.tree, + this.forceIgnore + ); } private getBundleName(contentPath: string): string { + if (isWebAppBaseType(contentPath)) { + const pathParts = contentPath.split(sep); + const digitalExperiencesIndex = getDigitalExperiencesIndex(contentPath); + const baseType = pathParts[digitalExperiencesIndex + 1]; + const spaceApiName = pathParts[digitalExperiencesIndex + 2]; + return `${baseType}/${spaceApiName}`; + } const bundlePath = this.getBundleMetadataXmlPath(contentPath); return `${parentName(dirname(bundlePath))}/${parentName(bundlePath)}`; } @@ -154,6 +239,9 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter { // if this is the bundle type and it ends with -meta.xml, then this is the bundle metadata xml path return path; } + if (isWebAppBaseType(path)) { + return ''; + } const pathParts = path.split(sep); const typeFolderIndex = pathParts.lastIndexOf(this.type.directoryName); // 3 because we want 'digitalExperiences' directory, 'baseType' directory and 'bundleName' directory @@ -177,3 +265,28 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter { const calculateNameFromPath = (contentPath: string): string => `${parentName(contentPath)}/${baseName(contentPath)}`; const digitalExperienceStructure = join('BaseType', 'SpaceApiName', 'ContentType', 'ContentApiName'); const contentParts = digitalExperienceStructure.split(sep); + +/** + * Checks if the given path belongs to the web_app base type. + * web_app base type has a simpler structure without ContentType folders. + * Structure: digitalExperiences/web_app/spaceApiName/...files... + */ +const isWebAppBaseType = (path: string): boolean => { + const pathParts = path.split(sep); + const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences'); + // Check if the base type (folder after digitalExperiences) is WEB_APP_BASE_TYPE + return ( + digitalExperiencesIndex > -1 && + pathParts.length > digitalExperiencesIndex + 1 && + pathParts[digitalExperiencesIndex + 1] === WEB_APP_BASE_TYPE + ); +}; + +/** + * Gets the digitalExperiences index from a path. + * Returns -1 if not found. + */ +const getDigitalExperiencesIndex = (path: string): number => { + const pathParts = path.split(sep); + return pathParts.indexOf('digitalExperiences'); +}; diff --git a/src/resolve/metadataResolver.ts b/src/resolve/metadataResolver.ts index 4349801d4..78e2e6c78 100644 --- a/src/resolve/metadataResolver.ts +++ b/src/resolve/metadataResolver.ts @@ -31,6 +31,20 @@ import { NodeFSTreeContainer, TreeContainer } from './treeContainers'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); +/** + * Checks if the given path is a web_app bundle directory. + * web_app bundles don't have metadata XML files and are identified by directory structure. + */ +const isWebAppBundlePath = (path: string): boolean => { + const pathParts = path.split(sep); + const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences'); + return ( + digitalExperiencesIndex > -1 && + pathParts.length > digitalExperiencesIndex + 2 && + pathParts[digitalExperiencesIndex + 1] === 'web_app' + ); +}; + /** * Resolver for metadata type and component objects. * @@ -224,6 +238,10 @@ const resolveDirectoryAsComponent = (registry: RegistryAccess) => (tree: TreeContainer) => (dirPath: string): boolean => { + if (isWebAppBundlePath(dirPath)) { + return true; + } + const type = resolveType(registry)(tree)(dirPath); if (type) { const { directoryName, inFolder } = type; @@ -335,6 +353,10 @@ const resolveType = (registry: RegistryAccess) => (tree: TreeContainer) => (fsPath: string): MetadataType | undefined => { + if (isWebAppBundlePath(fsPath)) { + return registry.getTypeByName('DigitalExperienceBundle'); + } + // attempt 1 - check if the file is part of a component that requires a strict type folder let resolvedType = resolveTypeFromStrictFolder(registry)(fsPath); diff --git a/test/client/metadataApiDeploy.test.ts b/test/client/metadataApiDeploy.test.ts index c8da492d7..e59a1bd8a 100644 --- a/test/client/metadataApiDeploy.test.ts +++ b/test/client/metadataApiDeploy.test.ts @@ -1218,6 +1218,57 @@ describe('MetadataApiDeploy', () => { expect(responses).to.deep.equal(expected); }); + it('should return FileResponses for web_app DigitalExperienceBundle with child files', () => { + const bundlePath = join('path', 'to', 'digitalExperiences', 'web_app', 'zenith'); + const props = { + name: 'web_app/zenith', + type: registry.types.digitalexperiencebundle, + content: bundlePath, + }; + const component = SourceComponent.createVirtualComponent(props, [ + { + dirPath: bundlePath, + children: ['index.html', 'app.js', 'style.css'], + }, + ]); + const deployedSet = new ComponentSet([component]); + const { fullName, type } = component; + const apiStatus: Partial = { + details: { + componentSuccesses: { + changed: 'false', + created: 'true', + deleted: 'false', + success: 'true', + fullName, + componentType: type.name, + } as DeployMessage, + }, + }; + const result = new DeployResult(apiStatus as MetadataApiDeployStatus, deployedSet); + + const responses = result.getFileResponses(); + + // Should have 1 bundle response + 3 file responses + expect(responses).to.have.lengthOf(4); + + // First response should be the bundle + expect(responses[0]).to.deep.include({ + fullName: 'web_app/zenith', + type: 'DigitalExperienceBundle', + state: ComponentStatus.Created, + filePath: bundlePath, + }); + + // Remaining responses should be DigitalExperience child files + const childResponses = responses.slice(1); + childResponses.forEach((response) => { + expect(response.type).to.equal('DigitalExperience'); + expect(response.fullName).to.match(/^web_app\/zenith\//); + expect(response.state).to.equal(ComponentStatus.Created); + }); + }); + it('should cache fileResponses', () => { const component = COMPONENT; const deployedSet = new ComponentSet([component]); diff --git a/test/resolve/adapters/digitalExperienceSourceAdapter.test.ts b/test/resolve/adapters/digitalExperienceSourceAdapter.test.ts index b0ad7c38d..b143174e7 100644 --- a/test/resolve/adapters/digitalExperienceSourceAdapter.test.ts +++ b/test/resolve/adapters/digitalExperienceSourceAdapter.test.ts @@ -192,4 +192,26 @@ describe('DigitalExperienceSourceAdapter', () => { }); }); }); + + describe('DigitalExperienceSourceAdapter for web_app base type', () => { + const WEBAPP_BUNDLE_PATH = join(BASE_PATH, 'web_app', 'zenith'); + const WEBAPP_CSS_FILE = join(WEBAPP_BUNDLE_PATH, 'css', 'home.css'); + + const webappTree = VirtualTreeContainer.fromFilePaths([WEBAPP_CSS_FILE]); + + const webappBundleAdapter = new DigitalExperienceSourceAdapter( + registry.types.digitalexperiencebundle, + registryAccess, + forceIgnore, + webappTree + ); + + it('should return a SourceComponent for web_app bundle directory (no meta.xml required)', () => { + const component = webappBundleAdapter.getComponent(WEBAPP_BUNDLE_PATH); + expect(component).to.not.be.undefined; + expect(component?.type.name).to.equal('DigitalExperienceBundle'); + expect(component?.fullName).to.equal('web_app/zenith'); + expect(component?.content).to.equal(WEBAPP_BUNDLE_PATH); + }); + }); }); diff --git a/test/resolve/metadataResolver.test.ts b/test/resolve/metadataResolver.test.ts index a04bf978e..d63b1874f 100644 --- a/test/resolve/metadataResolver.test.ts +++ b/test/resolve/metadataResolver.test.ts @@ -266,6 +266,22 @@ describe('MetadataResolver', () => { expect(mdResolver.getComponentsFromPath(path)).to.deep.equal([expectedComponent]); }); + it('Should determine type for web_app DigitalExperienceBundle (no meta.xml required)', () => { + const bundlePath = join('unpackaged', 'digitalExperiences', 'web_app', 'zenith'); + const filePath = join(bundlePath, 'index.html'); + const treeContainer = VirtualTreeContainer.fromFilePaths([filePath]); + const mdResolver = new MetadataResolver(undefined, treeContainer); + const expectedComponent = new SourceComponent( + { + name: 'web_app/zenith', + type: registry.types.digitalexperiencebundle, + content: bundlePath, + }, + treeContainer + ); + expect(mdResolver.getComponentsFromPath(bundlePath)).to.deep.equal([expectedComponent]); + }); + it('Should determine type for path of mixed content type', () => { const path = mixedContentDirectory.MIXED_CONTENT_DIRECTORY_SOURCE_PATHS[1]; const access = testUtil.createMetadataResolver([ From 4d658b5b5ca00cdab0023ba937080809f3c131c7 Mon Sep 17 00:00:00 2001 From: pyelchuri Date: Mon, 10 Nov 2025 12:19:02 +0530 Subject: [PATCH 2/9] refactor: Remove dead code in parseMetadataXml The second if block checking web_app paths can never execute because: - web_app bundles don't have meta.xml files - super.parseMetadataXml() returns undefined for web_app paths - The condition 'xml && isWebAppBaseType(path)' is always false --- src/resolve/adapters/digitalExperienceSourceAdapter.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/resolve/adapters/digitalExperienceSourceAdapter.ts b/src/resolve/adapters/digitalExperienceSourceAdapter.ts index 75cb07b50..97623375e 100644 --- a/src/resolve/adapters/digitalExperienceSourceAdapter.ts +++ b/src/resolve/adapters/digitalExperienceSourceAdapter.ts @@ -191,13 +191,6 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter { path: xml.path, }; } - if (xml && isWebAppBaseType(path)) { - return { - fullName: this.getBundleName(path), - suffix: xml.suffix, - path: xml.path, - }; - } } private populateWebAppBundle(trigger: string, component?: SourceComponent): SourceComponent { From fbbf61519cd105374e2874ab49232bd8041e13f8 Mon Sep 17 00:00:00 2001 From: pyelchuri Date: Mon, 10 Nov 2025 16:59:15 +0530 Subject: [PATCH 3/9] refactor: optimize populateWebAppBundle logic - Reduced path splitting from 3 times to 1 time - Removed unused pathParts variable - Improved code clarity with inline comments --- .../adapters/digitalExperienceSourceAdapter.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/resolve/adapters/digitalExperienceSourceAdapter.ts b/src/resolve/adapters/digitalExperienceSourceAdapter.ts index 97623375e..45e81b81f 100644 --- a/src/resolve/adapters/digitalExperienceSourceAdapter.ts +++ b/src/resolve/adapters/digitalExperienceSourceAdapter.ts @@ -197,13 +197,24 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter { if (component) { return component; } - const bundleName = this.getBundleName(trigger); + const pathParts = trigger.split(sep); - const bundleDir = pathParts.slice(0, getDigitalExperiencesIndex(trigger) + 3).join(sep); + const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences'); + + // Extract bundle name: web_app/WebApp3 + const baseType = pathParts[digitalExperiencesIndex + 1]; + const spaceApiName = pathParts[digitalExperiencesIndex + 2]; + const bundleName = `${baseType}/${spaceApiName}`; + + // Extract bundle directory: /path/to/digitalExperiences/web_app/WebApp3 + const bundleDir = pathParts.slice(0, digitalExperiencesIndex + 3).join(sep); + + // Get the DigitalExperienceBundle type const parentType = this.isBundleType() ? this.type : this.registry.getParentType(this.type.id); if (!parentType) { throw messages.createError('error_failed_convert', [bundleName]); } + return new SourceComponent( { name: bundleName, From 673d7d3cab84eaf985ff8880fc65ad23204190a4 Mon Sep 17 00:00:00 2001 From: pyelchuri Date: Thu, 13 Nov 2025 01:12:51 +0530 Subject: [PATCH 4/9] refactor: addressed review comments --- src/client/deployMessages.ts | 17 +++++++++-------- .../digitalExperienceSourceAdapter.ts | 17 ++++++----------- src/resolve/metadataResolver.ts | 19 +++---------------- 3 files changed, 18 insertions(+), 35 deletions(-) diff --git a/src/client/deployMessages.ts b/src/client/deployMessages.ts index b60d27c50..8fe999dde 100644 --- a/src/client/deployMessages.ts +++ b/src/client/deployMessages.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { basename, dirname, extname, join, posix, sep } from 'node:path/posix'; +import { basename, dirname, extname, join, posix, relative, sep } from 'node:path/posix'; import { SfError } from '@salesforce/core/sfError'; import { ensureArray } from '@salesforce/kit'; import { ComponentLike, SourceComponent } from '../resolve'; @@ -87,10 +87,11 @@ export const createResponses = (component: SourceComponent, responseMessages: De if (state === ComponentStatus.Failed) { return [{ ...base, state, ...parseDeployDiagnostic(component, message) } satisfies FileResponseFailure]; } else { - const isWebAppBundle = component.type.name === 'DigitalExperienceBundle' && - component.fullName.startsWith('web_app/') && - component.content; - + const isWebAppBundle = + component.type.name === 'DigitalExperienceBundle' && + component.fullName.startsWith('web_app/') && + component.content; + if (isWebAppBundle) { const walkedPaths = component.walkContent(); const bundleResponse: FileResponseSuccess = { @@ -100,9 +101,9 @@ export const createResponses = (component: SourceComponent, responseMessages: De filePath: component.content!, }; const fileResponses: FileResponseSuccess[] = walkedPaths.map((filePath) => { - const relPath = filePath.replace(component.content! + '/', ''); + const relPath = relative(component.content!, filePath); return { - fullName: `${component.fullName}/${relPath}`, + fullName: join(component.fullName, relPath).split(sep).join(posix.sep), type: 'DigitalExperience', state, filePath, @@ -110,7 +111,7 @@ export const createResponses = (component: SourceComponent, responseMessages: De }); return [bundleResponse, ...fileResponses]; } - + return [ ...(shouldWalkContent(component) ? component.walkContent().map((filePath): FileResponseSuccess => ({ ...base, state, filePath })) diff --git a/src/resolve/adapters/digitalExperienceSourceAdapter.ts b/src/resolve/adapters/digitalExperienceSourceAdapter.ts index 45e81b81f..25fd7dd55 100644 --- a/src/resolve/adapters/digitalExperienceSourceAdapter.ts +++ b/src/resolve/adapters/digitalExperienceSourceAdapter.ts @@ -201,10 +201,10 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter { const pathParts = trigger.split(sep); const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences'); - // Extract bundle name: web_app/WebApp3 + // Extract bundle name: web_app/WebApp3 (always use posix separator for metadata names) const baseType = pathParts[digitalExperiencesIndex + 1]; const spaceApiName = pathParts[digitalExperiencesIndex + 2]; - const bundleName = `${baseType}/${spaceApiName}`; + const bundleName = [baseType, spaceApiName].join('/'); // Extract bundle directory: /path/to/digitalExperiences/web_app/WebApp3 const bundleDir = pathParts.slice(0, digitalExperiencesIndex + 3).join(sep); @@ -232,10 +232,10 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter { const digitalExperiencesIndex = getDigitalExperiencesIndex(contentPath); const baseType = pathParts[digitalExperiencesIndex + 1]; const spaceApiName = pathParts[digitalExperiencesIndex + 2]; - return `${baseType}/${spaceApiName}`; + return [baseType, spaceApiName].join('/'); } const bundlePath = this.getBundleMetadataXmlPath(contentPath); - return `${parentName(dirname(bundlePath))}/${parentName(bundlePath)}`; + return [parentName(dirname(bundlePath)), parentName(bundlePath)].join('/'); } private getBundleMetadataXmlPath(path: string): string { @@ -275,15 +275,10 @@ const contentParts = digitalExperienceStructure.split(sep); * web_app base type has a simpler structure without ContentType folders. * Structure: digitalExperiences/web_app/spaceApiName/...files... */ -const isWebAppBaseType = (path: string): boolean => { +export const isWebAppBaseType = (path: string): boolean => { const pathParts = path.split(sep); const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences'); - // Check if the base type (folder after digitalExperiences) is WEB_APP_BASE_TYPE - return ( - digitalExperiencesIndex > -1 && - pathParts.length > digitalExperiencesIndex + 1 && - pathParts[digitalExperiencesIndex + 1] === WEB_APP_BASE_TYPE - ); + return pathParts[digitalExperiencesIndex + 1] === WEB_APP_BASE_TYPE; }; /** diff --git a/src/resolve/metadataResolver.ts b/src/resolve/metadataResolver.ts index 78e2e6c78..893e9b345 100644 --- a/src/resolve/metadataResolver.ts +++ b/src/resolve/metadataResolver.ts @@ -27,24 +27,11 @@ import { SourceAdapterFactory } from './adapters/sourceAdapterFactory'; import { ForceIgnore } from './forceIgnore'; import { SourceComponent } from './sourceComponent'; import { NodeFSTreeContainer, TreeContainer } from './treeContainers'; +import { isWebAppBaseType } from './adapters/digitalExperienceSourceAdapter'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); -/** - * Checks if the given path is a web_app bundle directory. - * web_app bundles don't have metadata XML files and are identified by directory structure. - */ -const isWebAppBundlePath = (path: string): boolean => { - const pathParts = path.split(sep); - const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences'); - return ( - digitalExperiencesIndex > -1 && - pathParts.length > digitalExperiencesIndex + 2 && - pathParts[digitalExperiencesIndex + 1] === 'web_app' - ); -}; - /** * Resolver for metadata type and component objects. * @@ -238,7 +225,7 @@ const resolveDirectoryAsComponent = (registry: RegistryAccess) => (tree: TreeContainer) => (dirPath: string): boolean => { - if (isWebAppBundlePath(dirPath)) { + if (isWebAppBaseType(dirPath)) { return true; } @@ -353,7 +340,7 @@ const resolveType = (registry: RegistryAccess) => (tree: TreeContainer) => (fsPath: string): MetadataType | undefined => { - if (isWebAppBundlePath(fsPath)) { + if (isWebAppBaseType(fsPath)) { return registry.getTypeByName('DigitalExperienceBundle'); } From f60b078b3e878058eb83773f9da178359895dd37 Mon Sep 17 00:00:00 2001 From: pyelchuri Date: Thu, 13 Nov 2025 15:08:44 +0530 Subject: [PATCH 5/9] test: added snapshot tests for webapp - Added new snapshot test for DigitalExperienceBundle with web_app base type - Fixed merge conversion bug where files were placed at wrong directory level - Refactored duplicate code in digitalExperienceSourceAdapter - Added getWebAppBundleDir helper function to eliminate duplication --- .../defaultMetadataTransformer.ts | 4 +- .../digitalExperienceSourceAdapter.ts | 20 ++- .../web_app/WebApp/README.md | 50 +++++++ .../web_app/WebApp/package.json | 28 ++++ .../web_app/WebApp/public/index.html | 17 +++ .../web_app/WebApp/src/App.css | 35 +++++ .../web_app/WebApp/src/App.js | 23 ++++ .../web_app/WebApp/src/index.css | 13 ++ .../web_app/WebApp/src/index.js | 11 ++ .../verify-md-files.expected/package.xml | 8 ++ .../web_app/WebApp/README.md | 50 +++++++ .../web_app/WebApp/package.json | 28 ++++ .../web_app/WebApp/public/index.html | 17 +++ .../web_app/WebApp/src/App.css | 35 +++++ .../web_app/WebApp/src/App.js | 23 ++++ .../web_app/WebApp/src/index.css | 13 ++ .../web_app/WebApp/src/index.js | 11 ++ .../web_app/WebApp/README.md | 50 +++++++ .../web_app/WebApp/package.json | 28 ++++ .../web_app/WebApp/public/index.html | 14 ++ .../web_app/WebApp/src/App.css | 35 +++++ .../web_app/WebApp/src/App.js | 21 +++ .../web_app/WebApp/src/index.css | 11 ++ .../web_app/WebApp/src/index.js | 11 ++ .../originalMdapi/package.xml | 9 ++ .../sfdx-project.json | 12 ++ .../snapshots.test.ts | 124 ++++++++++++++++++ 27 files changed, 698 insertions(+), 3 deletions(-) create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/README.md create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/package.json create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/public/index.html create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.css create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.js create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.css create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.js create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/package.xml create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/README.md create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/package.json create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/public/index.html create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.css create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.js create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.css create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.js create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/README.md create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/package.json create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/public/index.html create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/App.css create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/App.js create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/index.css create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/index.js create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/package.xml create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/sfdx-project.json create mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/snapshots.test.ts diff --git a/src/convert/transformers/defaultMetadataTransformer.ts b/src/convert/transformers/defaultMetadataTransformer.ts index e8ea01dec..308a07d91 100644 --- a/src/convert/transformers/defaultMetadataTransformer.ts +++ b/src/convert/transformers/defaultMetadataTransformer.ts @@ -85,7 +85,9 @@ const getContentSourceDestination = ( if (mergeWith?.content) { if (component.content && component.tree.isDirectory(component.content)) { // DEs are always inside a dir. - if (component.type.strategies?.adapter === 'digitalExperience') { + // For web_app base type, use standard relative path logic (no ContentType folders) + const isWebApp = source.includes(`${sep}web_app${sep}`); + if (component.type.strategies?.adapter === 'digitalExperience' && !isWebApp) { const parts = source.split(sep); const file = parts.pop() ?? ''; const dir = join(mergeWith.content, parts.pop() ?? ''); diff --git a/src/resolve/adapters/digitalExperienceSourceAdapter.ts b/src/resolve/adapters/digitalExperienceSourceAdapter.ts index 25fd7dd55..ebcc2afe7 100644 --- a/src/resolve/adapters/digitalExperienceSourceAdapter.ts +++ b/src/resolve/adapters/digitalExperienceSourceAdapter.ts @@ -125,7 +125,8 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter { return path; } if (isWebAppBaseType(path)) { - return path; + // For web_app, trim to the bundle directory: digitalExperiences/web_app/WebApp + return getWebAppBundleDir(path); } const pathToContent = dirname(path); const parts = pathToContent.split(sep); @@ -207,7 +208,7 @@ export class DigitalExperienceSourceAdapter extends BundleSourceAdapter { const bundleName = [baseType, spaceApiName].join('/'); // Extract bundle directory: /path/to/digitalExperiences/web_app/WebApp3 - const bundleDir = pathParts.slice(0, digitalExperiencesIndex + 3).join(sep); + const bundleDir = getWebAppBundleDir(trigger); // Get the DigitalExperienceBundle type const parentType = this.isBundleType() ? this.type : this.registry.getParentType(this.type.id); @@ -289,3 +290,18 @@ const getDigitalExperiencesIndex = (path: string): number => { const pathParts = path.split(sep); return pathParts.indexOf('digitalExperiences'); }; + +/** + * Gets the web_app bundle directory path. + * For a path like: /path/to/digitalExperiences/web_app/WebApp/src/App.js + * Returns: /path/to/digitalExperiences/web_app/WebApp + */ +const getWebAppBundleDir = (path: string): string => { + const pathParts = path.split(sep); + const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences'); + if (digitalExperiencesIndex > -1 && pathParts.length > digitalExperiencesIndex + 3) { + // Return up to digitalExperiences/web_app/spaceApiName + return pathParts.slice(0, digitalExperiencesIndex + 3).join(sep); + } + return path; +}; diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/README.md b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/README.md new file mode 100644 index 000000000..1c38b5007 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/README.md @@ -0,0 +1,50 @@ +# WebApp2 + +This is a dummy React application created for demonstration purposes. + +## Available Scripts + +In the project directory, you can run: + +### `npm install` + +Installs all the dependencies required for the project. + +### `npm start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in your browser. + +The page will reload when you make changes.\ +You may also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode. + +### `npm run build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +## Getting Started + +1. Install dependencies: `npm install` +2. Start the development server: `npm start` +3. Open your browser to [http://localhost:3000](http://localhost:3000) + +## Project Structure + +``` +WebApp2/ +├── public/ +│ └── index.html +├── src/ +│ ├── App.js +│ ├── App.css +│ ├── index.js +│ └── index.css +├── package.json +├── .gitignore +└── README.md +``` diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/package.json b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/package.json new file mode 100644 index 000000000..9f2ecda99 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/package.json @@ -0,0 +1,28 @@ +{ + "name": "webapp2", + "version": "0.1.0", + "private": true, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/public/index.html b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/public/index.html new file mode 100644 index 000000000..2d074390f --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + WebApp2 + + + +
+ + diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.css b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.css new file mode 100644 index 000000000..a3a8a2d39 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.css @@ -0,0 +1,35 @@ +.App { + text-align: center; +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.card { + padding: 2rem; + background-color: #1a1d23; + border-radius: 8px; + margin-top: 2rem; + max-width: 600px; +} + +.card h2 { + margin-top: 0; + color: #61dafb; +} + +code { + background-color: #1a1d23; + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-family: 'Courier New', monospace; + color: #61dafb; +} diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.js b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.js new file mode 100644 index 000000000..42d544a26 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.js @@ -0,0 +1,23 @@ +import React from 'react'; +import './App.css'; + +function App() { + return ( +
+
+

Welcome to WebApp2

+

+ This is a dummy React application. +

+
+

Getting Started

+

+ Edit src/App.js and save to reload. +

+
+
+
+ ); +} + +export default App; diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.css b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.css new file mode 100644 index 000000000..ec2585e8c --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.js b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.js new file mode 100644 index 000000000..2cb1087e7 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/package.xml b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/package.xml new file mode 100644 index 000000000..7e129a32d --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/package.xml @@ -0,0 +1,8 @@ + + + + web_app/ + DigitalExperienceBundle + + 64.0 + diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/README.md b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/README.md new file mode 100644 index 000000000..1c38b5007 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/README.md @@ -0,0 +1,50 @@ +# WebApp2 + +This is a dummy React application created for demonstration purposes. + +## Available Scripts + +In the project directory, you can run: + +### `npm install` + +Installs all the dependencies required for the project. + +### `npm start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in your browser. + +The page will reload when you make changes.\ +You may also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode. + +### `npm run build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +## Getting Started + +1. Install dependencies: `npm install` +2. Start the development server: `npm start` +3. Open your browser to [http://localhost:3000](http://localhost:3000) + +## Project Structure + +``` +WebApp2/ +├── public/ +│ └── index.html +├── src/ +│ ├── App.js +│ ├── App.css +│ ├── index.js +│ └── index.css +├── package.json +├── .gitignore +└── README.md +``` diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/package.json b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/package.json new file mode 100644 index 000000000..9f2ecda99 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/package.json @@ -0,0 +1,28 @@ +{ + "name": "webapp2", + "version": "0.1.0", + "private": true, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/public/index.html b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/public/index.html new file mode 100644 index 000000000..2d074390f --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + WebApp2 + + + +
+ + diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.css b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.css new file mode 100644 index 000000000..a3a8a2d39 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.css @@ -0,0 +1,35 @@ +.App { + text-align: center; +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.card { + padding: 2rem; + background-color: #1a1d23; + border-radius: 8px; + margin-top: 2rem; + max-width: 600px; +} + +.card h2 { + margin-top: 0; + color: #61dafb; +} + +code { + background-color: #1a1d23; + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-family: 'Courier New', monospace; + color: #61dafb; +} diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.js b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.js new file mode 100644 index 000000000..42d544a26 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.js @@ -0,0 +1,23 @@ +import React from 'react'; +import './App.css'; + +function App() { + return ( +
+
+

Welcome to WebApp2

+

+ This is a dummy React application. +

+
+

Getting Started

+

+ Edit src/App.js and save to reload. +

+
+
+
+ ); +} + +export default App; diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.css b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.css new file mode 100644 index 000000000..ec2585e8c --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.js b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.js new file mode 100644 index 000000000..2cb1087e7 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/README.md b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/README.md new file mode 100644 index 000000000..1c38b5007 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/README.md @@ -0,0 +1,50 @@ +# WebApp2 + +This is a dummy React application created for demonstration purposes. + +## Available Scripts + +In the project directory, you can run: + +### `npm install` + +Installs all the dependencies required for the project. + +### `npm start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in your browser. + +The page will reload when you make changes.\ +You may also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode. + +### `npm run build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +## Getting Started + +1. Install dependencies: `npm install` +2. Start the development server: `npm start` +3. Open your browser to [http://localhost:3000](http://localhost:3000) + +## Project Structure + +``` +WebApp2/ +├── public/ +│ └── index.html +├── src/ +│ ├── App.js +│ ├── App.css +│ ├── index.js +│ └── index.css +├── package.json +├── .gitignore +└── README.md +``` diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/package.json b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/package.json new file mode 100644 index 000000000..9f2ecda99 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/package.json @@ -0,0 +1,28 @@ +{ + "name": "webapp2", + "version": "0.1.0", + "private": true, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/public/index.html b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/public/index.html new file mode 100644 index 000000000..231dd3c87 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/public/index.html @@ -0,0 +1,14 @@ + + + + + + + + WebApp2 + + + +
+ + diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/App.css b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/App.css new file mode 100644 index 000000000..a3a8a2d39 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/App.css @@ -0,0 +1,35 @@ +.App { + text-align: center; +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.card { + padding: 2rem; + background-color: #1a1d23; + border-radius: 8px; + margin-top: 2rem; + max-width: 600px; +} + +.card h2 { + margin-top: 0; + color: #61dafb; +} + +code { + background-color: #1a1d23; + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-family: 'Courier New', monospace; + color: #61dafb; +} diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/App.js b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/App.js new file mode 100644 index 000000000..f39510ee9 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/App.js @@ -0,0 +1,21 @@ +import React from 'react'; +import './App.css'; + +function App() { + return ( +
+
+

Welcome to WebApp2

+

This is a dummy React application.

+
+

Getting Started

+

+ Edit src/App.js and save to reload. +

+
+
+
+ ); +} + +export default App; diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/index.css b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/index.css new file mode 100644 index 000000000..7323ae85c --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/index.css @@ -0,0 +1,11 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', + 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; +} diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/index.js b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/index.js new file mode 100644 index 000000000..2cb1087e7 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/package.xml b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/package.xml new file mode 100644 index 000000000..6e326fcd8 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/package.xml @@ -0,0 +1,9 @@ + + + + web_app/WebApp + DigitalExperienceBundle + + 64.0 + + diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/sfdx-project.json b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/sfdx-project.json new file mode 100644 index 000000000..04448b855 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/sfdx-project.json @@ -0,0 +1,12 @@ +{ + "packageDirectories": [ + { + "path": "force-app", + "default": true + } + ], + "name": "digitalExperienceBundleWithWebappsProject", + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "64.0" +} diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/snapshots.test.ts b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/snapshots.test.ts new file mode 100644 index 000000000..981e77ee9 --- /dev/null +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/snapshots.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { expect } from 'chai'; +import { + FORCE_APP, + MDAPI_OUT, + dirsAreIdentical, + fileSnap, + mdapiToSource, + sourceToMdapi, +} from '../../helper/conversions'; +import { ComponentSetBuilder, MetadataConverter, RegistryAccess } from '../../../../src'; + +// we don't want failing tests outputting over each other +/* eslint-disable no-await-in-loop */ + +describe('digitalExperienceBundleWithWebapps', () => { + const testDir = path.join('test', 'snapshot', 'sampleProjects', 'digitalExperienceBundleWithWebapps'); + let sourceFiles: string[]; + let mdFiles: string[]; + + before(async () => { + await fs.promises.mkdir(path.join(testDir, FORCE_APP), { recursive: true }); + sourceFiles = await mdapiToSource(testDir); + mdFiles = await sourceToMdapi(testDir); + }); + it('verify source files', async () => { + for (const file of sourceFiles) { + await fileSnap(file, testDir); + } + await dirsAreIdentical( + path.join(testDir, FORCE_APP), + path.join(testDir, '__snapshots__', 'verify-source-files.expected', FORCE_APP) + ); + }); + + it('verifies source files after two conversions', async () => { + // reads all files in a directory recursively + function getAllFiles(dirPath: string, fileList: string[] = []): string[] { + const files = fs.readdirSync(dirPath); + + for (const file of files) { + const filePath = path.join(dirPath, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + getAllFiles(filePath, fileList); // Recursive call for subdirectories + } else { + fileList.push(filePath); + } + } + + return fileList; + } + + // should contain correct file path for webapps + const fileList = getAllFiles(path.join(testDir, FORCE_APP)); + + await dirsAreIdentical( + path.join(testDir, FORCE_APP), + path.join(testDir, '__snapshots__', 'verify-source-files.expected', FORCE_APP) + ); + + // build a new CS from the freshly-converted source-format metadata + const cs = await ComponentSetBuilder.build({ + sourcepath: [path.join(testDir, 'originalMdapi')], + projectDir: testDir, + }); + const registry = new RegistryAccess(undefined, testDir); + const converter = new MetadataConverter(registry); + + // converts metadata format DEB into source-format, with a mergeWith option, merging into force-app + await converter.convert( + cs, + 'source', // loads custom registry if there is one + { + type: 'merge', + mergeWith: ( + await ComponentSetBuilder.build({ + sourcepath: [path.join(testDir, 'force-app')], + projectDir: testDir, + }) + ).getSourceComponents(), + defaultDirectory: path.join(testDir, 'force-app'), + } + ); + + // verify the file list is consistent after two conversions + const fileList2 = getAllFiles(path.join(testDir, FORCE_APP)); + expect(fileList2).to.deep.equal(fileList); + + await dirsAreIdentical( + path.join(testDir, FORCE_APP), + path.join(testDir, '__snapshots__', 'verify-source-files.expected', FORCE_APP) + ); + }); + it('verify md files', async () => { + for (const file of mdFiles) { + await fileSnap(file, testDir); + } + }); + + after(async () => { + await Promise.all([ + fs.promises.rm(path.join(testDir, FORCE_APP), { recursive: true, force: true }), + fs.promises.rm(path.join(testDir, MDAPI_OUT), { recursive: true, force: true }), + ]); + }); +}); From 2e6cf2b23a2c62a5f20d7b26dad16c66a98b58df Mon Sep 17 00:00:00 2001 From: pyelchuri Date: Thu, 13 Nov 2025 21:06:08 +0530 Subject: [PATCH 6/9] test: added snapshot tests --- src/resolve/metadataResolver.ts | 7 ++++++- .../digitalExperiences/web_app/WebApp/public/index.html | 5 +---- .../digitalExperiences/web_app/WebApp/src/App.js | 4 +--- .../digitalExperiences/web_app/WebApp/src/index.css | 8 +++----- .../__snapshots__/verify-md-files.expected/package.xml | 2 +- .../digitalExperiences/web_app/WebApp/public/index.html | 5 +---- .../default/digitalExperiences/web_app/WebApp/src/App.js | 4 +--- .../digitalExperiences/web_app/WebApp/src/index.css | 8 +++----- 8 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/resolve/metadataResolver.ts b/src/resolve/metadataResolver.ts index 893e9b345..e3a0c1c69 100644 --- a/src/resolve/metadataResolver.ts +++ b/src/resolve/metadataResolver.ts @@ -225,8 +225,13 @@ const resolveDirectoryAsComponent = (registry: RegistryAccess) => (tree: TreeContainer) => (dirPath: string): boolean => { + // For web_app bundles, only the bundle directory itself should be resolved as a component + // (e.g., digitalExperiences/web_app/WebApp), not subdirectories like src/, public/, etc. if (isWebAppBaseType(dirPath)) { - return true; + const pathParts = dirPath.split(sep); + const digitalExperiencesIndex = pathParts.indexOf('digitalExperiences'); + // The bundle directory is exactly 3 levels deep: digitalExperiences/web_app/bundleName + return digitalExperiencesIndex !== -1 && pathParts.length === digitalExperiencesIndex + 3; } const type = resolveType(registry)(tree)(dirPath); diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/public/index.html b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/public/index.html index 2d074390f..231dd3c87 100644 --- a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/public/index.html +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/public/index.html @@ -4,10 +4,7 @@ - + WebApp2 diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.js b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.js index 42d544a26..f39510ee9 100644 --- a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.js +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/App.js @@ -6,9 +6,7 @@ function App() {

Welcome to WebApp2

-

- This is a dummy React application. -

+

This is a dummy React application.

Getting Started

diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.css b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.css index ec2585e8c..7323ae85c 100644 --- a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.css +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/src/index.css @@ -1,13 +1,11 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', + 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/package.xml b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/package.xml index 7e129a32d..cd80b82e2 100644 --- a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/package.xml +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/package.xml @@ -1,7 +1,7 @@ - web_app/ + web_app/WebApp DigitalExperienceBundle 64.0 diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/public/index.html b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/public/index.html index 2d074390f..231dd3c87 100644 --- a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/public/index.html +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/public/index.html @@ -4,10 +4,7 @@ - + WebApp2 diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.js b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.js index 42d544a26..f39510ee9 100644 --- a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.js +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/App.js @@ -6,9 +6,7 @@ function App() {

Welcome to WebApp2

-

- This is a dummy React application. -

+

This is a dummy React application.

Getting Started

diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.css b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.css index ec2585e8c..7323ae85c 100644 --- a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.css +++ b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/src/index.css @@ -1,13 +1,11 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', + 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } From e66354b82722d3ae35869e2bb1b542270f90ea2d Mon Sep 17 00:00:00 2001 From: pyelchuri Date: Thu, 13 Nov 2025 22:17:33 +0530 Subject: [PATCH 7/9] test: removed unnecessary test data --- .../web_app/WebApp/README.md | 50 ------------------- .../web_app/WebApp/README.md | 50 ------------------- .../web_app/WebApp/README.md | 50 ------------------- 3 files changed, 150 deletions(-) delete mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/README.md delete mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/README.md delete mode 100644 test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/README.md diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/README.md b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/README.md deleted file mode 100644 index 1c38b5007..000000000 --- a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-md-files.expected/digitalExperiences/web_app/WebApp/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# WebApp2 - -This is a dummy React application created for demonstration purposes. - -## Available Scripts - -In the project directory, you can run: - -### `npm install` - -Installs all the dependencies required for the project. - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. - -The page will reload when you make changes.\ -You may also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -## Getting Started - -1. Install dependencies: `npm install` -2. Start the development server: `npm start` -3. Open your browser to [http://localhost:3000](http://localhost:3000) - -## Project Structure - -``` -WebApp2/ -├── public/ -│ └── index.html -├── src/ -│ ├── App.js -│ ├── App.css -│ ├── index.js -│ └── index.css -├── package.json -├── .gitignore -└── README.md -``` diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/README.md b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/README.md deleted file mode 100644 index 1c38b5007..000000000 --- a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/__snapshots__/verify-source-files.expected/force-app/main/default/digitalExperiences/web_app/WebApp/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# WebApp2 - -This is a dummy React application created for demonstration purposes. - -## Available Scripts - -In the project directory, you can run: - -### `npm install` - -Installs all the dependencies required for the project. - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. - -The page will reload when you make changes.\ -You may also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -## Getting Started - -1. Install dependencies: `npm install` -2. Start the development server: `npm start` -3. Open your browser to [http://localhost:3000](http://localhost:3000) - -## Project Structure - -``` -WebApp2/ -├── public/ -│ └── index.html -├── src/ -│ ├── App.js -│ ├── App.css -│ ├── index.js -│ └── index.css -├── package.json -├── .gitignore -└── README.md -``` diff --git a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/README.md b/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/README.md deleted file mode 100644 index 1c38b5007..000000000 --- a/test/snapshot/sampleProjects/digitalExperienceBundleWithWebapps/originalMdapi/digitalExperiences/web_app/WebApp/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# WebApp2 - -This is a dummy React application created for demonstration purposes. - -## Available Scripts - -In the project directory, you can run: - -### `npm install` - -Installs all the dependencies required for the project. - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. - -The page will reload when you make changes.\ -You may also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -## Getting Started - -1. Install dependencies: `npm install` -2. Start the development server: `npm start` -3. Open your browser to [http://localhost:3000](http://localhost:3000) - -## Project Structure - -``` -WebApp2/ -├── public/ -│ └── index.html -├── src/ -│ ├── App.js -│ ├── App.css -│ ├── index.js -│ └── index.css -├── package.json -├── .gitignore -└── README.md -``` From f308b45fe7715c2cc7f62f1d24f7750c43858860 Mon Sep 17 00:00:00 2001 From: pyelchuri Date: Thu, 13 Nov 2025 23:00:13 +0530 Subject: [PATCH 8/9] fix: normalize paths for web_app file responses on Windows - Use posix.relative and posix.join to ensure consistent path separators - Fixes test failure on Windows CI where mixed path separators caused incorrect fullName generation - Child DigitalExperience components now get correct fullNames like 'web_app/zenith/index.html' --- src/client/deployMessages.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/client/deployMessages.ts b/src/client/deployMessages.ts index 8fe999dde..28a554f23 100644 --- a/src/client/deployMessages.ts +++ b/src/client/deployMessages.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { basename, dirname, extname, join, posix, relative, sep } from 'node:path/posix'; +import { basename, dirname, extname, join, posix, sep } from 'node:path'; import { SfError } from '@salesforce/core/sfError'; import { ensureArray } from '@salesforce/kit'; import { ComponentLike, SourceComponent } from '../resolve'; @@ -101,9 +101,12 @@ export const createResponses = (component: SourceComponent, responseMessages: De filePath: component.content!, }; const fileResponses: FileResponseSuccess[] = walkedPaths.map((filePath) => { - const relPath = relative(component.content!, filePath); + // Normalize paths to ensure relative() works correctly on Windows + const normalizedContent = component.content!.split(sep).join(posix.sep); + const normalizedFilePath = filePath.split(sep).join(posix.sep); + const relPath = posix.relative(normalizedContent, normalizedFilePath); return { - fullName: join(component.fullName, relPath).split(sep).join(posix.sep), + fullName: posix.join(component.fullName, relPath), type: 'DigitalExperience', state, filePath, From 393506c59149a3619b758adfc24227ba041c06cd Mon Sep 17 00:00:00 2001 From: pyelchuri Date: Thu, 13 Nov 2025 23:11:31 +0530 Subject: [PATCH 9/9] fix: add web_app file responses for retrieve operations - Retrieve now reports individual files for web_app bundles - Fixes 'Nothing retrieved' warning when retrieving web_app bundles - Matches the deploy behavior for consistent file response reporting --- src/client/metadataApiRetrieve.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/client/metadataApiRetrieve.ts b/src/client/metadataApiRetrieve.ts index 554395edc..51a2e6d28 100644 --- a/src/client/metadataApiRetrieve.ts +++ b/src/client/metadataApiRetrieve.ts @@ -101,14 +101,25 @@ export class RetrieveResult implements MetadataTransferResult { // construct successes for (const retrievedComponent of this.components.getSourceComponents()) { - const { fullName, type, xml } = retrievedComponent; + const { fullName, type, xml, content } = retrievedComponent; const baseResponse = { fullName, type: type.name, state: this.localComponents.has(retrievedComponent) ? ComponentStatus.Changed : ComponentStatus.Created, } as const; - if (!type.children || Object.values(type.children.types).some((t) => t.unaddressableWithoutParent)) { + // Special handling for web_app bundles - they need to walk content and report individual files + const isWebAppBundle = type.name === 'DigitalExperienceBundle' && fullName.startsWith('web_app/') && content; + + if (isWebAppBundle) { + const walkedPaths = retrievedComponent.walkContent(); + // Add the bundle directory itself + this.fileResponses.push({ ...baseResponse, filePath: content } satisfies FileResponseSuccess); + // Add each file with its specific path + for (const filePath of walkedPaths) { + this.fileResponses.push({ ...baseResponse, filePath } satisfies FileResponseSuccess); + } + } else if (!type.children || Object.values(type.children.types).some((t) => t.unaddressableWithoutParent)) { for (const filePath of retrievedComponent.walkContent()) { this.fileResponses.push({ ...baseResponse, filePath } satisfies FileResponseSuccess); }