From 8f238d7a24eb9bfe45465c65ddd09aad2121f7ca Mon Sep 17 00:00:00 2001 From: Aparna Goyal Date: Tue, 22 Sep 2026 02:22:04 +0530 Subject: [PATCH 1/2] feat: support Data Cloud dataspace-scoped metadata types Add SDR support for Data Cloud dataspace-scoped metadata (CalculatedInsight, DataModelObject). Their on-disk shape differs from every existing type: each component is a pair of generic .json files - a payload .json and a .meta.json sidecar - nested under a d360/dataspace/// wrapper, with no -meta.xml. The component fullName is dataspace-scoped (.). - registry: register the two types with dataspaceScoped adapter/transformer strategies and index the `json` suffix so bare .json files resolve as metadata. - resolve: DataspaceScopedSourceAdapter derives the . fullName from the path and folds the .meta.json sidecar into its payload component. - convert: DataspaceScopedMetadataTransformer preserves the full d360/... path in both directions and co-writes the sidecar alongside the payload. - collections: expandDataspaceScopedComponentSet builds the minimal deploy closure from each component's sidecar retrieveWith set. --- .../dataspaceScopedDependencies.ts | 148 ++++++++++++++++++ src/collections/index.ts | 1 + .../dataspaceScopedMetadataTransformer.ts | 87 ++++++++++ .../metadataTransformerFactory.ts | 3 + src/index.ts | 1 + src/registry/metadataRegistry.json | 27 ++++ src/registry/types.ts | 6 +- .../adapters/dataspaceScopedSourceAdapter.ts | 122 +++++++++++++++ src/resolve/adapters/index.ts | 1 + src/resolve/adapters/sourceAdapterFactory.ts | 3 + test/registry/registryValidation.test.ts | 1 + 11 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 src/collections/dataspaceScopedDependencies.ts create mode 100644 src/convert/transformers/dataspaceScopedMetadataTransformer.ts create mode 100644 src/resolve/adapters/dataspaceScopedSourceAdapter.ts diff --git a/src/collections/dataspaceScopedDependencies.ts b/src/collections/dataspaceScopedDependencies.ts new file mode 100644 index 0000000000..1d7c59293b --- /dev/null +++ b/src/collections/dataspaceScopedDependencies.ts @@ -0,0 +1,148 @@ +/* + * Copyright 2026, 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 { RegistryAccess } from '../registry/registryAccess'; +import { SourceComponent } from '../resolve/sourceComponent'; +import { ComponentSet } from './componentSet'; + +/** + * Any Data Cloud dataspace-scoped component is stored as a PAIR of files: a payload `.json` + * carrying the component's definition, and a `.meta.json` sidecar carrying + * `{ componentType, componentName, dataspaceName, retrieveWith, dependsOn }`. This module is + * type-agnostic — it works off the sidecar convention alone, so it covers every dataspace-scoped + * type without per-type logic. + * + * The sidecar declares TWO distinct relationship lists, and they mean different things. + * `retrieveWith` is the set of components that travel together with this one as a single deployable + * unit — the deploy closure that {@link expandDataspaceScopedComponentSet} walks. `dependsOn` is + * informational metadata about what this component references (the components it points at) and is + * NOT used to build the deploy closure. + * + * Both lists reference the target component's `componentName` — NOT the SDR fullName + * (e.g. `.`). Resolution therefore matches on each component's sidecar + * `componentName`, which every dataspace-scoped type carries regardless of how its payload is shaped + * (payload shapes vary by type — some are objects, some are bare arrays). + * + * This module is intentionally standalone: it does NOT modify `ComponentSet` or any deploy + * machinery. A caller (the CLI plugin) resolves the full project into `full`, decides what the + * user requested into `requested`, and calls {@link expandDataspaceScopedComponentSet} to obtain + * the exact closure to deploy — the requested components plus every dataspace-scoped component in + * their transitive `retrieveWith` set, and nothing else. + */ + +/** The adapter strategy id shared by every dataspace-scoped type. */ +const DATASPACE_SCOPED_ADAPTER = 'dataspaceScoped'; + +/** Filename suffix of the per-component metadata sidecar (`.meta.json`). */ +const METADATA_SIDECAR_SUFFIX = '.meta.json'; + +type ComponentRef = { componentType?: string; componentName?: string }; + +type ComponentSidecar = { + componentType?: string; + componentName?: string; + dataspaceName?: string; + /** Components that deploy together with this one as one unit — drives the deploy closure. */ + retrieveWith?: ComponentRef[]; + /** Informational: what this component references (e.g. source DMOs). Not used for the closure. */ + dependsOn?: ComponentRef[]; +}; + +const isDataspaceScoped = (component: SourceComponent): boolean => + component.type.strategies?.adapter === DATASPACE_SCOPED_ADAPTER; + +const readJson = (component: SourceComponent, path: string | undefined): T | undefined => { + if (!path || !component.tree.exists(path)) { + return undefined; + } + try { + return JSON.parse(component.tree.readFileSync(path).toString()) as T; + } catch { + return undefined; + } +}; + +/** The `.meta.json` sidecar sitting next to the payload content file. */ +const getSidecarPath = (component: SourceComponent): string | undefined => + component.content?.replace(/\.json$/, METADATA_SIDECAR_SUFFIX); + +/** The component's own identity: the sidecar `componentName` (the key space that `retrieveWith` references). */ +const getComponentName = (component: SourceComponent): string | undefined => + readJson(component, getSidecarPath(component))?.componentName; + +/** The `componentName` values this component lists in its sidecar `retrieveWith` (its deploy-closure peers). */ +const getRetrieveWithNames = (component: SourceComponent): string[] => + (readJson(component, getSidecarPath(component))?.retrieveWith ?? []) + .map((entry) => entry.componentName) + .filter((name): name is string => Boolean(name)); + +/** + * Build the minimal deploy closure for dataspace-scoped components. + * + * The result is the requested components plus the transitive closure of their dataspace-scoped + * `retrieveWith` peers (resolved via `retrieveWith` -> sidecar `componentName`), and nothing else. + * Non-dataspace-scoped requested components pass through unchanged, so this is safe to call on any set. + * + * @param full A ComponentSet with every candidate component (the whole project); only its dataspace-scoped members are indexed for lookup. + * @param requested The components the user asked to deploy. + * @param registry Optional RegistryAccess to seed the resulting ComponentSet with (defaults to a fresh one). + * @returns A new ComponentSet containing the requested components plus their dataspace-scoped retrieveWith closure. + */ +export const expandDataspaceScopedComponentSet = ( + full: ComponentSet, + requested: ComponentSet, + registry?: RegistryAccess +): ComponentSet => { + // Index dataspace-scoped candidates by their sidecar componentName (the retrieveWith key space). + const byComponentName = new Map(); + for (const component of full.getSourceComponents()) { + if (isDataspaceScoped(component)) { + const componentName = getComponentName(component); + if (componentName) { + byComponentName.set(componentName, component); + } + } + } + + const result = new ComponentSet([], registry); + const seen = new Set(); + const worklist: SourceComponent[] = []; + + // Seed with everything the user requested; non-dataspace-scoped members are kept as-is. + for (const component of requested.getSourceComponents()) { + if (!seen.has(component)) { + seen.add(component); + result.add(component); + if (isDataspaceScoped(component)) { + worklist.push(component); + } + } + } + + // Walk retrieveWith transitively, pulling in only the referenced dataspace-scoped components. + while (worklist.length) { + const component = worklist.pop()!; + for (const peerName of getRetrieveWithNames(component)) { + const peer = byComponentName.get(peerName); + if (peer && !seen.has(peer)) { + seen.add(peer); + result.add(peer); + worklist.push(peer); + } + } + } + + return result; +}; diff --git a/src/collections/index.ts b/src/collections/index.ts index 04bd794c69..24cd6a1aa1 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -23,3 +23,4 @@ export { FromManifestOptions, } from './types'; export { ComponentSetBuilder, ComponentSetOptions } from './componentSetBuilder'; +export { expandDataspaceScopedComponentSet } from './dataspaceScopedDependencies'; diff --git a/src/convert/transformers/dataspaceScopedMetadataTransformer.ts b/src/convert/transformers/dataspaceScopedMetadataTransformer.ts new file mode 100644 index 0000000000..6c33c24fd4 --- /dev/null +++ b/src/convert/transformers/dataspaceScopedMetadataTransformer.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2026, 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 { join } from 'node:path'; +import { DEFAULT_PACKAGE_ROOT_SFDX } from '../../common/constants'; +import { SourcePath } from '../../common/types'; +import { trimUntil } from '../../utils/path'; +import { METADATA_SIDECAR_SUFFIX } from '../../resolve/adapters/dataspaceScopedSourceAdapter'; +import { getReplacementStreamForReadable } from '../replacements'; +import { WriteInfo } from '../types'; +import { SourceComponent } from '../../resolve/sourceComponent'; +import { BaseMetadataTransformer } from './baseMetadataTransformer'; + +// The Data Cloud wrapper root that must be preserved on disk and in the mdapi package. The full +// wire path is `d360/dataspace///.json`; we anchor on `d360` so the whole +// dataspace-scoped structure below it is kept verbatim. +const DATASPACE_ROOT = 'd360'; + +/** + * Transformer for any Data Cloud dataspace-scoped type. It is type-agnostic — it operates purely on + * the `d360/dataspace///` path shape and the sidecar convention, so it applies + * unchanged to every dataspace-scoped type; onboarding a new one is a registry change, not a code change. + * + * Each component is a PAIR of generic `.json` files nested under a + * `d360/dataspace///` wrapper: the payload `.json` (the component's + * content) and a `.meta.json` sidecar (`{ componentType, componentName, dataspaceName, + * retrieveWith, dependsOn }`). The stock `calculateRelativePath` would collapse the path to + * `/.json` (dropping the `d360/dataspace//` prefix), so this transformer instead + * preserves the whole path from `d360` down and co-writes the sidecar. The layout is identical in + * both directions: + * + * - source format: `main/default/d360/dataspace///.json` (+ `.meta.json`) + * - metadata format: `d360/dataspace///.json` (+ `.meta.json`) + */ +export class DataspaceScopedMetadataTransformer extends BaseMetadataTransformer { + // eslint-disable-next-line @typescript-eslint/require-await, class-methods-use-this + public async toMetadataFormat(component: SourceComponent): Promise { + return getWriteInfos(component, 'metadata'); + } + + // eslint-disable-next-line @typescript-eslint/require-await, class-methods-use-this + public async toSourceFormat({ component }: { component: SourceComponent }): Promise { + return getWriteInfos(component, 'source'); + } +} + +const getWriteInfos = (component: SourceComponent, targetFormat: 'source' | 'metadata'): WriteInfo[] => + // walkContent() yields only the payload file(s); for each we also emit its sibling + // `.meta.json` sidecar when present, so both halves of the pair are written. + component.walkContent().flatMap((path) => { + const infos: WriteInfo[] = [ + { + source: getReplacementStreamForReadable(component, path), + output: getDataspaceScopedDestination(path, targetFormat), + }, + ]; + const sidecar = path.replace(/\.json$/, METADATA_SIDECAR_SUFFIX); + if (sidecar !== path && component.tree.exists(sidecar)) { + infos.push({ + source: getReplacementStreamForReadable(component, sidecar), + output: getDataspaceScopedDestination(sidecar, targetFormat), + }); + } + return infos; + }); + +/** + * Build the destination path preserving the `d360/dataspace///.json` structure. + * Source format is rooted under `main/default`; metadata format keeps it at the package root. + */ +const getDataspaceScopedDestination = (source: SourcePath, targetFormat: 'source' | 'metadata'): SourcePath => { + const base = targetFormat === 'source' ? DEFAULT_PACKAGE_ROOT_SFDX : ''; + // trimUntil keeps the path from `d360` onward (dataspace wrapper + type dir + file). + return join(base, trimUntil(source, DATASPACE_ROOT, true)); +}; diff --git a/src/convert/transformers/metadataTransformerFactory.ts b/src/convert/transformers/metadataTransformerFactory.ts index 7f875260d1..88b50c47b6 100644 --- a/src/convert/transformers/metadataTransformerFactory.ts +++ b/src/convert/transformers/metadataTransformerFactory.ts @@ -26,6 +26,7 @@ import { LabelMetadataTransformer, LabelsMetadataTransformer } from './decompose import { DecomposedPermissionSetTransformer } from './decomposedPermissionSetTransformer'; import { DecomposeExternalServiceRegistrationTransformer } from './decomposeExternalServiceRegistrationTransformer'; import { UiBundleMetadataTransformer } from './uiBundleMetadataTransformer'; +import { DataspaceScopedMetadataTransformer } from './dataspaceScopedMetadataTransformer'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); @@ -60,6 +61,8 @@ export class MetadataTransformerFactory { return new DecomposeExternalServiceRegistrationTransformer(this.registry, this.context); case 'uiBundle': return new UiBundleMetadataTransformer(this.registry, this.context); + case 'dataspaceScoped': + return new DataspaceScopedMetadataTransformer(this.registry, this.context); default: throw messages.createError('error_missing_transformer', [type.name, transformerId]); } diff --git a/src/index.ts b/src/index.ts index a23a937cb1..8d47fe8318 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,6 +100,7 @@ export { DestructiveChangesType, FromSourceOptions, FromManifestOptions, + expandDataspaceScopedComponentSet, } from './collections'; export { diff --git a/src/registry/metadataRegistry.json b/src/registry/metadataRegistry.json index 57bb335297..c84f9d9f50 100644 --- a/src/registry/metadataRegistry.json +++ b/src/registry/metadataRegistry.json @@ -51,7 +51,9 @@ "botBlocks": "botblock", "botTemplates": "bottemplate", "bots": "bot", + "calculatedInsight": "calculatedinsight", "contentTypes": "contenttypebundle", + "mktDataModelObject": "datamodelobject", "documents": "document", "emailservices": "emailservicesfunction", "experiencePropertyTypeBundles": "experiencepropertytypebundle", @@ -344,6 +346,7 @@ "integrationProviderDefinition": "integrationproviderdef", "internalDataConnector": "internaldataconnector", "internalOrganization": "internalorganization", + "json": "calculatedinsight", "keywords": "keywordlist", "labels": "customlabels", "layout": "layout", @@ -5422,6 +5425,30 @@ "adapter": "bundle" }, "supportsPartialDelete": true + }, + "calculatedinsight": { + "id": "calculatedinsight", + "name": "CalculatedInsight", + "suffix": "json", + "directoryName": "calculatedInsight", + "inFolder": false, + "strictDirectoryName": true, + "strategies": { + "adapter": "dataspaceScoped", + "transformer": "dataspaceScoped" + } + }, + "datamodelobject": { + "id": "datamodelobject", + "name": "DataModelObject", + "suffix": "json", + "directoryName": "mktDataModelObject", + "inFolder": false, + "strictDirectoryName": true, + "strategies": { + "adapter": "dataspaceScoped", + "transformer": "dataspaceScoped" + } } } } diff --git a/src/registry/types.ts b/src/registry/types.ts index dc68fa7372..31e77c81a3 100644 --- a/src/registry/types.ts +++ b/src/registry/types.ts @@ -161,7 +161,8 @@ export type MetadataType = { | 'bundle' | 'default' | 'partiallyDecomposed' - | 'uiBundles'; + | 'uiBundles' + | 'dataspaceScoped'; transformer?: | 'decomposed' | 'staticResource' @@ -170,7 +171,8 @@ export type MetadataType = { | 'decomposedLabels' | 'decomposedPermissionSet' | 'decomposeExternalServiceRegistration' - | 'uiBundle'; + | 'uiBundle' + | 'dataspaceScoped'; decomposition?: 'topLevel' | 'folderPerType'; recomposition?: 'startEmpty'; }; diff --git a/src/resolve/adapters/dataspaceScopedSourceAdapter.ts b/src/resolve/adapters/dataspaceScopedSourceAdapter.ts new file mode 100644 index 0000000000..cc60626e30 --- /dev/null +++ b/src/resolve/adapters/dataspaceScopedSourceAdapter.ts @@ -0,0 +1,122 @@ +/* + * Copyright 2026, 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 { sep } from 'node:path'; +import { Messages } from '@salesforce/core/messages'; +import { SfError } from '@salesforce/core/sfError'; +import { baseName } from '../../utils/path'; +import { SourcePath } from '../../common/types'; +import { MetadataXml } from '../types'; +import { SourceComponent } from '../sourceComponent'; +import { MixedContentSourceAdapter } from './mixedContentSourceAdapter'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); + +/** Filename suffix of the per-component metadata sidecar (`.meta.json`). */ +export const METADATA_SIDECAR_SUFFIX = '.meta.json'; + +/** + * Handles any Data Cloud dataspace-scoped type whose layout nests a pair of JSON files per + * component under a `d360/dataspace///` wrapper. This adapter is type-agnostic: + * it derives everything from the path shape and the registered type, so onboarding a new + * dataspace-scoped type is a registry change (a type entry + directory mapping), never a code change. + * + * Each component is TWO files: the payload `.json` (the component's definition) and a sidecar + * `.meta.json` (`{ componentType, componentName, dataspaceName, retrieveWith, dependsOn }`). + * There is no `-meta.xml`. The payload file is the component's content; the sidecar rides along as an + * extra file (written by the transformer) and is NOT resolved as a separate component. + * + * The component fullName is dataspace-scoped: `.` (matching how the CLI + * addresses it, e.g. `:.`), where `` is the path + * segment immediately above the type's directory. + * + * __Example Structure__ (`` is the registered `directoryName` of any dataspace-scoped type): + * + *```text + * d360/dataspace/ + * ├── / + * | ├── / + * | | ├── .json -> :. + * | | ├── .meta.json -> sidecar (not its own component) + *``` + */ +export class DataspaceScopedSourceAdapter extends MixedContentSourceAdapter { + // Each component is a single JSON file; there is no separate metadata xml. + protected metadataWithContent = false; + + /** + * The single JSON file IS the content, not a root metadata xml. Returning undefined here + * (and from {@link getRootMetadataXmlPath}) ensures the base `getComponent` does NOT + * pre-build a SourceComponent with a plain, non-dataspace-scoped name — instead `populate` + * builds it with the correct `.` fullName. + */ + // eslint-disable-next-line class-methods-use-this + protected parseAsRootMetadataXml(): MetadataXml | undefined { + return undefined; + } + + // eslint-disable-next-line class-methods-use-this + protected getRootMetadataXmlPath(): SourcePath | undefined { + return undefined; + } + + protected populate(trigger: SourcePath, component?: SourceComponent): SourceComponent | undefined { + const contentPath = this.trimPathToContent(trigger); + // The `.meta.json` sidecar is part of its sibling payload component, not a component of + // its own. Skip it here so resolution never yields a spurious component; the transformer emits + // the sidecar alongside the payload. This exclusion is REQUIRED: `baseName` splits on the first + // `.`, so `.json` and `.meta.json` both reduce to `` — without it the sidecar + // would resolve to the same dataspace-scoped name as the payload and collide. + if (contentPath.endsWith(METADATA_SIDECAR_SUFFIX)) { + return undefined; + } + if (!contentPath || !this.tree.exists(contentPath)) { + throw new SfError( + messages.getMessage('error_expected_source_files', [trigger, this.type.name]), + 'ExpectedSourceFilesError' + ); + } + + const name = this.calculateDataspaceScopedName(contentPath); + if (component) { + component.content = contentPath; + } else { + component = new SourceComponent( + { + name, + type: this.type, + content: contentPath, + }, + this.tree, + this.forceIgnore + ); + } + return component; + } + + /** + * Build `.` from a path shaped like + * `.../dataSpaces///.json`. The dataspace is the path segment + * immediately preceding the type's directory. + */ + private calculateDataspaceScopedName(contentPath: SourcePath): string { + const pathParts = contentPath.split(sep); + const typeFolderIndex = pathParts.lastIndexOf(this.type.directoryName); + const dataspace = typeFolderIndex > 0 ? pathParts[typeFolderIndex - 1] : undefined; + const shortName = baseName(contentPath); + return dataspace ? `${dataspace}.${shortName}` : shortName; + } +} diff --git a/src/resolve/adapters/index.ts b/src/resolve/adapters/index.ts index 44ac84cf18..77beeff7b1 100644 --- a/src/resolve/adapters/index.ts +++ b/src/resolve/adapters/index.ts @@ -21,3 +21,4 @@ export { DefaultSourceAdapter } from './defaultSourceAdapter'; export { BaseSourceAdapter } from './baseSourceAdapter'; export { DigitalExperienceSourceAdapter } from './digitalExperienceSourceAdapter'; export { UiBundlesSourceAdapter } from './uiBundlesSourceAdapter'; +export { DataspaceScopedSourceAdapter } from './dataspaceScopedSourceAdapter'; diff --git a/src/resolve/adapters/sourceAdapterFactory.ts b/src/resolve/adapters/sourceAdapterFactory.ts index 591d75f2c3..9c32c70793 100644 --- a/src/resolve/adapters/sourceAdapterFactory.ts +++ b/src/resolve/adapters/sourceAdapterFactory.ts @@ -28,6 +28,7 @@ import { DefaultSourceAdapter } from './defaultSourceAdapter'; import { DigitalExperienceSourceAdapter } from './digitalExperienceSourceAdapter'; import { UiBundlesSourceAdapter } from './uiBundlesSourceAdapter'; import { PartialDecomposedAdapter } from './partialDecomposedAdapter'; +import { DataspaceScopedSourceAdapter } from './dataspaceScopedSourceAdapter'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); @@ -58,6 +59,8 @@ export class SourceAdapterFactory { return new UiBundlesSourceAdapter(type, this.registry, forceIgnore, this.tree); case 'partiallyDecomposed': return new PartialDecomposedAdapter(type, this.registry, forceIgnore, this.tree); + case 'dataspaceScoped': + return new DataspaceScopedSourceAdapter(type, this.registry, forceIgnore, this.tree); case 'default': case undefined: return new DefaultSourceAdapter(type, this.registry, forceIgnore, this.tree); diff --git a/test/registry/registryValidation.test.ts b/test/registry/registryValidation.test.ts index b794905e5e..9a79b1380a 100644 --- a/test/registry/registryValidation.test.ts +++ b/test/registry/registryValidation.test.ts @@ -294,6 +294,7 @@ describe('will run preset tests', () => { 'partiallyDecomposed', 'digitalExperience', 'uiBundles', + 'dataspaceScoped', ]).includes(type.strategies?.adapter); }); }); From d2eb72f794f19a95b50a1bc6d98da80cb8d22c19 Mon Sep 17 00:00:00 2001 From: Aparna Goyal Date: Wed, 23 Sep 2026 02:00:38 +0530 Subject: [PATCH 2/2] refactor: rename dataspace-scoped adapter, transformer, and closure to D360 --- ...eScopedDependencies.ts => d360Dependencies.ts} | 15 +++++++-------- src/collections/index.ts | 2 +- ...aTransformer.ts => d360MetadataTransformer.ts} | 10 +++++----- .../transformers/metadataTransformerFactory.ts | 6 +++--- src/index.ts | 2 +- src/registry/metadataRegistry.json | 8 ++++---- src/registry/types.ts | 4 ++-- ...copedSourceAdapter.ts => d360SourceAdapter.ts} | 6 +++--- src/resolve/adapters/index.ts | 2 +- src/resolve/adapters/sourceAdapterFactory.ts | 6 +++--- test/registry/registryValidation.test.ts | 2 +- 11 files changed, 31 insertions(+), 32 deletions(-) rename src/collections/{dataspaceScopedDependencies.ts => d360Dependencies.ts} (92%) rename src/convert/transformers/{dataspaceScopedMetadataTransformer.ts => d360MetadataTransformer.ts} (91%) rename src/resolve/adapters/{dataspaceScopedSourceAdapter.ts => d360SourceAdapter.ts} (96%) diff --git a/src/collections/dataspaceScopedDependencies.ts b/src/collections/d360Dependencies.ts similarity index 92% rename from src/collections/dataspaceScopedDependencies.ts rename to src/collections/d360Dependencies.ts index 1d7c59293b..b24ad57912 100644 --- a/src/collections/dataspaceScopedDependencies.ts +++ b/src/collections/d360Dependencies.ts @@ -26,7 +26,7 @@ import { ComponentSet } from './componentSet'; * * The sidecar declares TWO distinct relationship lists, and they mean different things. * `retrieveWith` is the set of components that travel together with this one as a single deployable - * unit — the deploy closure that {@link expandDataspaceScopedComponentSet} walks. `dependsOn` is + * unit — the deploy closure that {@link expandD360ComponentSet} walks. `dependsOn` is * informational metadata about what this component references (the components it points at) and is * NOT used to build the deploy closure. * @@ -37,13 +37,13 @@ import { ComponentSet } from './componentSet'; * * This module is intentionally standalone: it does NOT modify `ComponentSet` or any deploy * machinery. A caller (the CLI plugin) resolves the full project into `full`, decides what the - * user requested into `requested`, and calls {@link expandDataspaceScopedComponentSet} to obtain + * user requested into `requested`, and calls {@link expandD360ComponentSet} to obtain * the exact closure to deploy — the requested components plus every dataspace-scoped component in * their transitive `retrieveWith` set, and nothing else. */ /** The adapter strategy id shared by every dataspace-scoped type. */ -const DATASPACE_SCOPED_ADAPTER = 'dataspaceScoped'; +const D360_ADAPTER = 'd360'; /** Filename suffix of the per-component metadata sidecar (`.meta.json`). */ const METADATA_SIDECAR_SUFFIX = '.meta.json'; @@ -60,8 +60,7 @@ type ComponentSidecar = { dependsOn?: ComponentRef[]; }; -const isDataspaceScoped = (component: SourceComponent): boolean => - component.type.strategies?.adapter === DATASPACE_SCOPED_ADAPTER; +const isD360 = (component: SourceComponent): boolean => component.type.strategies?.adapter === D360_ADAPTER; const readJson = (component: SourceComponent, path: string | undefined): T | undefined => { if (!path || !component.tree.exists(path)) { @@ -100,7 +99,7 @@ const getRetrieveWithNames = (component: SourceComponent): string[] => * @param registry Optional RegistryAccess to seed the resulting ComponentSet with (defaults to a fresh one). * @returns A new ComponentSet containing the requested components plus their dataspace-scoped retrieveWith closure. */ -export const expandDataspaceScopedComponentSet = ( +export const expandD360ComponentSet = ( full: ComponentSet, requested: ComponentSet, registry?: RegistryAccess @@ -108,7 +107,7 @@ export const expandDataspaceScopedComponentSet = ( // Index dataspace-scoped candidates by their sidecar componentName (the retrieveWith key space). const byComponentName = new Map(); for (const component of full.getSourceComponents()) { - if (isDataspaceScoped(component)) { + if (isD360(component)) { const componentName = getComponentName(component); if (componentName) { byComponentName.set(componentName, component); @@ -125,7 +124,7 @@ export const expandDataspaceScopedComponentSet = ( if (!seen.has(component)) { seen.add(component); result.add(component); - if (isDataspaceScoped(component)) { + if (isD360(component)) { worklist.push(component); } } diff --git a/src/collections/index.ts b/src/collections/index.ts index 24cd6a1aa1..ec71285c9d 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -23,4 +23,4 @@ export { FromManifestOptions, } from './types'; export { ComponentSetBuilder, ComponentSetOptions } from './componentSetBuilder'; -export { expandDataspaceScopedComponentSet } from './dataspaceScopedDependencies'; +export { expandD360ComponentSet } from './d360Dependencies'; diff --git a/src/convert/transformers/dataspaceScopedMetadataTransformer.ts b/src/convert/transformers/d360MetadataTransformer.ts similarity index 91% rename from src/convert/transformers/dataspaceScopedMetadataTransformer.ts rename to src/convert/transformers/d360MetadataTransformer.ts index 6c33c24fd4..7911bb4c59 100644 --- a/src/convert/transformers/dataspaceScopedMetadataTransformer.ts +++ b/src/convert/transformers/d360MetadataTransformer.ts @@ -17,7 +17,7 @@ import { join } from 'node:path'; import { DEFAULT_PACKAGE_ROOT_SFDX } from '../../common/constants'; import { SourcePath } from '../../common/types'; import { trimUntil } from '../../utils/path'; -import { METADATA_SIDECAR_SUFFIX } from '../../resolve/adapters/dataspaceScopedSourceAdapter'; +import { METADATA_SIDECAR_SUFFIX } from '../../resolve/adapters/d360SourceAdapter'; import { getReplacementStreamForReadable } from '../replacements'; import { WriteInfo } from '../types'; import { SourceComponent } from '../../resolve/sourceComponent'; @@ -44,7 +44,7 @@ const DATASPACE_ROOT = 'd360'; * - source format: `main/default/d360/dataspace///.json` (+ `.meta.json`) * - metadata format: `d360/dataspace///.json` (+ `.meta.json`) */ -export class DataspaceScopedMetadataTransformer extends BaseMetadataTransformer { +export class D360MetadataTransformer extends BaseMetadataTransformer { // eslint-disable-next-line @typescript-eslint/require-await, class-methods-use-this public async toMetadataFormat(component: SourceComponent): Promise { return getWriteInfos(component, 'metadata'); @@ -63,14 +63,14 @@ const getWriteInfos = (component: SourceComponent, targetFormat: 'source' | 'met const infos: WriteInfo[] = [ { source: getReplacementStreamForReadable(component, path), - output: getDataspaceScopedDestination(path, targetFormat), + output: getD360Destination(path, targetFormat), }, ]; const sidecar = path.replace(/\.json$/, METADATA_SIDECAR_SUFFIX); if (sidecar !== path && component.tree.exists(sidecar)) { infos.push({ source: getReplacementStreamForReadable(component, sidecar), - output: getDataspaceScopedDestination(sidecar, targetFormat), + output: getD360Destination(sidecar, targetFormat), }); } return infos; @@ -80,7 +80,7 @@ const getWriteInfos = (component: SourceComponent, targetFormat: 'source' | 'met * Build the destination path preserving the `d360/dataspace///.json` structure. * Source format is rooted under `main/default`; metadata format keeps it at the package root. */ -const getDataspaceScopedDestination = (source: SourcePath, targetFormat: 'source' | 'metadata'): SourcePath => { +const getD360Destination = (source: SourcePath, targetFormat: 'source' | 'metadata'): SourcePath => { const base = targetFormat === 'source' ? DEFAULT_PACKAGE_ROOT_SFDX : ''; // trimUntil keeps the path from `d360` onward (dataspace wrapper + type dir + file). return join(base, trimUntil(source, DATASPACE_ROOT, true)); diff --git a/src/convert/transformers/metadataTransformerFactory.ts b/src/convert/transformers/metadataTransformerFactory.ts index 88b50c47b6..de521bf57e 100644 --- a/src/convert/transformers/metadataTransformerFactory.ts +++ b/src/convert/transformers/metadataTransformerFactory.ts @@ -26,7 +26,7 @@ import { LabelMetadataTransformer, LabelsMetadataTransformer } from './decompose import { DecomposedPermissionSetTransformer } from './decomposedPermissionSetTransformer'; import { DecomposeExternalServiceRegistrationTransformer } from './decomposeExternalServiceRegistrationTransformer'; import { UiBundleMetadataTransformer } from './uiBundleMetadataTransformer'; -import { DataspaceScopedMetadataTransformer } from './dataspaceScopedMetadataTransformer'; +import { D360MetadataTransformer } from './d360MetadataTransformer'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); @@ -61,8 +61,8 @@ export class MetadataTransformerFactory { return new DecomposeExternalServiceRegistrationTransformer(this.registry, this.context); case 'uiBundle': return new UiBundleMetadataTransformer(this.registry, this.context); - case 'dataspaceScoped': - return new DataspaceScopedMetadataTransformer(this.registry, this.context); + case 'd360': + return new D360MetadataTransformer(this.registry, this.context); default: throw messages.createError('error_missing_transformer', [type.name, transformerId]); } diff --git a/src/index.ts b/src/index.ts index 8d47fe8318..4b52a0e1c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,7 +100,7 @@ export { DestructiveChangesType, FromSourceOptions, FromManifestOptions, - expandDataspaceScopedComponentSet, + expandD360ComponentSet, } from './collections'; export { diff --git a/src/registry/metadataRegistry.json b/src/registry/metadataRegistry.json index c84f9d9f50..a222e674e9 100644 --- a/src/registry/metadataRegistry.json +++ b/src/registry/metadataRegistry.json @@ -5434,8 +5434,8 @@ "inFolder": false, "strictDirectoryName": true, "strategies": { - "adapter": "dataspaceScoped", - "transformer": "dataspaceScoped" + "adapter": "d360", + "transformer": "d360" } }, "datamodelobject": { @@ -5446,8 +5446,8 @@ "inFolder": false, "strictDirectoryName": true, "strategies": { - "adapter": "dataspaceScoped", - "transformer": "dataspaceScoped" + "adapter": "d360", + "transformer": "d360" } } } diff --git a/src/registry/types.ts b/src/registry/types.ts index 31e77c81a3..f88ed69b87 100644 --- a/src/registry/types.ts +++ b/src/registry/types.ts @@ -162,7 +162,7 @@ export type MetadataType = { | 'default' | 'partiallyDecomposed' | 'uiBundles' - | 'dataspaceScoped'; + | 'd360'; transformer?: | 'decomposed' | 'staticResource' @@ -172,7 +172,7 @@ export type MetadataType = { | 'decomposedPermissionSet' | 'decomposeExternalServiceRegistration' | 'uiBundle' - | 'dataspaceScoped'; + | 'd360'; decomposition?: 'topLevel' | 'folderPerType'; recomposition?: 'startEmpty'; }; diff --git a/src/resolve/adapters/dataspaceScopedSourceAdapter.ts b/src/resolve/adapters/d360SourceAdapter.ts similarity index 96% rename from src/resolve/adapters/dataspaceScopedSourceAdapter.ts rename to src/resolve/adapters/d360SourceAdapter.ts index cc60626e30..ec7d8b5f79 100644 --- a/src/resolve/adapters/dataspaceScopedSourceAdapter.ts +++ b/src/resolve/adapters/d360SourceAdapter.ts @@ -53,7 +53,7 @@ export const METADATA_SIDECAR_SUFFIX = '.meta.json'; * | | ├── .meta.json -> sidecar (not its own component) *``` */ -export class DataspaceScopedSourceAdapter extends MixedContentSourceAdapter { +export class D360SourceAdapter extends MixedContentSourceAdapter { // Each component is a single JSON file; there is no separate metadata xml. protected metadataWithContent = false; @@ -90,7 +90,7 @@ export class DataspaceScopedSourceAdapter extends MixedContentSourceAdapter { ); } - const name = this.calculateDataspaceScopedName(contentPath); + const name = this.calculateD360Name(contentPath); if (component) { component.content = contentPath; } else { @@ -112,7 +112,7 @@ export class DataspaceScopedSourceAdapter extends MixedContentSourceAdapter { * `.../dataSpaces///.json`. The dataspace is the path segment * immediately preceding the type's directory. */ - private calculateDataspaceScopedName(contentPath: SourcePath): string { + private calculateD360Name(contentPath: SourcePath): string { const pathParts = contentPath.split(sep); const typeFolderIndex = pathParts.lastIndexOf(this.type.directoryName); const dataspace = typeFolderIndex > 0 ? pathParts[typeFolderIndex - 1] : undefined; diff --git a/src/resolve/adapters/index.ts b/src/resolve/adapters/index.ts index 77beeff7b1..70900caee5 100644 --- a/src/resolve/adapters/index.ts +++ b/src/resolve/adapters/index.ts @@ -21,4 +21,4 @@ export { DefaultSourceAdapter } from './defaultSourceAdapter'; export { BaseSourceAdapter } from './baseSourceAdapter'; export { DigitalExperienceSourceAdapter } from './digitalExperienceSourceAdapter'; export { UiBundlesSourceAdapter } from './uiBundlesSourceAdapter'; -export { DataspaceScopedSourceAdapter } from './dataspaceScopedSourceAdapter'; +export { D360SourceAdapter } from './d360SourceAdapter'; diff --git a/src/resolve/adapters/sourceAdapterFactory.ts b/src/resolve/adapters/sourceAdapterFactory.ts index 9c32c70793..5e0158c9da 100644 --- a/src/resolve/adapters/sourceAdapterFactory.ts +++ b/src/resolve/adapters/sourceAdapterFactory.ts @@ -28,7 +28,7 @@ import { DefaultSourceAdapter } from './defaultSourceAdapter'; import { DigitalExperienceSourceAdapter } from './digitalExperienceSourceAdapter'; import { UiBundlesSourceAdapter } from './uiBundlesSourceAdapter'; import { PartialDecomposedAdapter } from './partialDecomposedAdapter'; -import { DataspaceScopedSourceAdapter } from './dataspaceScopedSourceAdapter'; +import { D360SourceAdapter } from './d360SourceAdapter'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); @@ -59,8 +59,8 @@ export class SourceAdapterFactory { return new UiBundlesSourceAdapter(type, this.registry, forceIgnore, this.tree); case 'partiallyDecomposed': return new PartialDecomposedAdapter(type, this.registry, forceIgnore, this.tree); - case 'dataspaceScoped': - return new DataspaceScopedSourceAdapter(type, this.registry, forceIgnore, this.tree); + case 'd360': + return new D360SourceAdapter(type, this.registry, forceIgnore, this.tree); case 'default': case undefined: return new DefaultSourceAdapter(type, this.registry, forceIgnore, this.tree); diff --git a/test/registry/registryValidation.test.ts b/test/registry/registryValidation.test.ts index 9a79b1380a..04c7f4f84c 100644 --- a/test/registry/registryValidation.test.ts +++ b/test/registry/registryValidation.test.ts @@ -294,7 +294,7 @@ describe('will run preset tests', () => { 'partiallyDecomposed', 'digitalExperience', 'uiBundles', - 'dataspaceScoped', + 'd360', ]).includes(type.strategies?.adapter); }); });