From 51cbe848959f580c1b9a2e6816e8f33e89a2bd64 Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Thu, 15 Aug 2024 13:49:05 -0500 Subject: [PATCH 1/3] feat: custom label beta 2 (#1392) * feat: customLabels => customLabel files * test: move existing snapshot project * fix: things snapshots uncovered * test: snapshot for mdapi => source (simple) * test: expected md snapshot * feat: source to madapi finalizer * feat: sorted labels when recomposed * fix: remove original customLabelsBeta * test: remove wrong snapshot path * feat: show all decomposed labels in FileResponses * fix: handling single label * fix: empty string to remove props from normal registry * fix: provide mergeSet for toSourceFormat * fix: error for using -m CustomLabels with the preset * chore: rename file to match class name * test: injectable presets into reg loader * test: ut for label transformer * refactor: move xml parsing to shared const * test: ut for label (source) => labels (mdapi) * refactor: use a new name for the updated beta * refactor: restore the original beta * refactor: separate logging fn * chore: bump core * chore: allow v1 and v2 of CustomLabelsBeta * chore(release): 12.1.13-qa.0 [skip ci] * test: don't test with both CL presets together * fix: only emit variant telemetry when there are presets/variants * chore(release): 12.1.13-qa.1 [skip ci] * chore: manually bump pjson version * chore(release): 12.3.0-qa.1 [skip ci] --------- Co-authored-by: svc-cli-bot --- HANDBOOK.md | 10 + package.json | 6 +- src/Presets.md | 19 +- src/collections/componentSetBuilder.ts | 8 +- src/convert/convertContext/convertContext.ts | 3 +- .../decomposedLabelsFinalizer.ts | 76 +++++++ .../convertContext/recompositionFinalizer.ts | 3 +- src/convert/streams.ts | 14 +- .../transformers/baseMetadataTransformer.ts | 5 +- .../decomposeLabelsTransformer.ts | 53 +++++ .../decomposedMetadataTransformer.ts | 8 +- .../defaultMetadataTransformer.ts | 8 +- .../metadataTransformerFactory.ts | 14 +- .../nonDecomposedMetadataTransformer.ts | 5 +- .../staticResourceMetadataTransformer.ts | 4 +- src/convert/types.ts | 9 +- src/index.ts | 15 +- src/registry/index.ts | 8 +- .../presets/decomposeCustomLabelsBeta.json | 10 +- .../presets/decomposeCustomLabelsBeta2.json | 32 +++ src/registry/presets/presetMap.ts | 2 + src/registry/types.ts | 6 +- src/registry/variants.ts | 121 ++++++++--- src/resolve/sourceComponent.ts | 14 +- src/utils/filePathGenerator.ts | 5 +- src/utils/metadata.ts | 23 +- test/convert/streams.test.ts | 11 +- .../decomposedLabelsTransformer.test.ts | 175 +++++++++++++++ .../decomposedMetadataTransformer.test.ts | 28 +-- .../defaultMetadataTransformer.test.ts | 20 +- .../nonDecomposedMetadataTransformer.test.ts | 4 +- .../staticResourceMetadataTransformer.test.ts | 34 +-- .../decomposedCustomLabelsConstant.ts | 199 ++++++++++++++++++ test/registry/presetTesting.ts | 2 + test/registry/registryValidation.test.ts | 29 ++- test/snapshot/helper/conversions.ts | 16 +- .../labels/CustomLabels.labels-meta.xml | 24 --- .../verify-md-files.expected/package.xml | 6 +- .../CustomLabels/CustomLabels.labels-meta.xml | 2 - .../DeleteMe.label-meta.xml | 2 +- .../{CustomLabels => }/KeepMe1.label-meta.xml | 2 +- .../{CustomLabels => }/KeepMe2.label-meta.xml | 2 +- .../preset-decomposeLabels/sfdx-project.json | 4 +- yarn.lock | 41 +--- 44 files changed, 831 insertions(+), 251 deletions(-) create mode 100644 src/convert/convertContext/decomposedLabelsFinalizer.ts create mode 100644 src/convert/transformers/decomposeLabelsTransformer.ts create mode 100644 src/registry/presets/decomposeCustomLabelsBeta2.json create mode 100644 test/convert/transformers/decomposedLabelsTransformer.test.ts create mode 100644 test/mock/type-constants/decomposedCustomLabelsConstant.ts delete mode 100644 test/snapshot/sampleProjects/customLabels-simple/__snapshots__/verify-source-files.expected/force-app/test/snapshot/sampleProjects/customLabels-simple/force-app/main/default/labels/CustomLabels.labels-meta.xml delete mode 100644 test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/CustomLabels.labels-meta.xml rename test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/{CustomLabels => }/DeleteMe.label-meta.xml (78%) rename test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/{CustomLabels => }/KeepMe1.label-meta.xml (78%) rename test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/{CustomLabels => }/KeepMe2.label-meta.xml (78%) diff --git a/HANDBOOK.md b/HANDBOOK.md index 20c662365a..e80562e841 100644 --- a/HANDBOOK.md +++ b/HANDBOOK.md @@ -172,6 +172,16 @@ Be careful when instantiating classes (ex: ComponentSet) that will default a Reg **Updating presets** If you do need to update a preset to make a breaking change, it's better to copy it to a new preset and give it a unique name (ex: `decomposeFooV2`). This preserves the existing behavior for existing projects with the old preset. +Presets **can** remove strings from the default metadataRegistry by setting values to empty string ex: + +```json +{ + "childTypes": { + "somethingThatIsUsuallyAChild": "" + } +} +``` + ### Querying registry data While it’s perfectly fine to reference the registry export directly, the `RegistryAccess` class was created to make accessing the object a bit more streamlined. Querying types and searching the registry is oftentimes easier and cleaner this way and contains built-in checking for whether or not a metadata type exists. Here’s a comparison of using each: diff --git a/package.json b/package.json index d46922b15f..ac06f110c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/source-deploy-retrieve", - "version": "12.2.1", + "version": "12.3.0-qa.1", "description": "JavaScript library to run Salesforce metadata deploys and retrieves", "main": "lib/src/index.js", "author": "Salesforce", @@ -25,8 +25,8 @@ "node": ">=18.0.0" }, "dependencies": { - "@salesforce/core": "^8.3.0", - "@salesforce/kit": "^3.1.6", + "@salesforce/core": "^8.4.0", + "@salesforce/kit": "^3.2.1", "@salesforce/ts-types": "^2.0.12", "fast-levenshtein": "^3.0.0", "fast-xml-parser": "^4.4.1", diff --git a/src/Presets.md b/src/Presets.md index 793f8701ef..4f5101300f 100644 --- a/src/Presets.md +++ b/src/Presets.md @@ -64,10 +64,12 @@ Simple fields (ex: `fullName`) can remain in the top-level `Account.workflow-met ## `decomposeCustomLabelsBeta` +> This will definitely not become GA. Based on user feedback, we replaced it with `decomposeCustomLabelsBeta2` + CustomLabels are decomposed to a folder named `CustomLabels` the labels are then placed into individual files metadata format -`/labels/CustomLabels.customlabes-meta.xml` +`/labels/CustomLabels.customlabels-meta.xml` source format @@ -77,3 +79,18 @@ source format /labels/CustomLabels/b.label-meta.xml /labels/CustomLabels/c.label-meta.xml ``` + +## `decomposeCustomLabelsBeta2` + +CustomLabels are decomposed to a folder named `labels`; the labels are then placed into individual files. There is no top-level file. + +metadata format +`/labels/CustomLabels.customlabels-meta.xml` + +source format + +```txt +/labels/a.label-meta.xml +/labels/b.label-meta.xml +/labels/c.label-meta.xml +``` diff --git a/src/collections/componentSetBuilder.ts b/src/collections/componentSetBuilder.ts index 205f190c64..578d5f06f6 100644 --- a/src/collections/componentSetBuilder.ts +++ b/src/collections/componentSetBuilder.ts @@ -284,12 +284,8 @@ export const entryToTypeAndName = // split on the first colon, and then join the rest back together to support names that include colons const [typeName, ...name] = rawEntry.split(':'); const type = reg.getTypeByName(typeName.trim()); - const parent = reg.getParentType(type.name); - // If a user is requesting a child type that is unaddressable (more common with custom registries to create proper behavior) - // throw an error letting them know to use the entire parent instead - // or if they're requesting a COFT, unadressable without parent, don't throw because the parent could be requested - we don't know at this point - if (type.isAddressable === false && parent !== undefined && !type.unaddressableWithoutParent) { - throw new Error(`Cannot use this type, instead use ${parent.name}`); + if (type.name === 'CustomLabels' && type.strategies?.transformer === 'decomposedLabels') { + throw new Error('Use CustomLabel instead of CustomLabels for decomposed labels'); } return { type, metadataName: name.length ? name.join(':').trim() : '*' }; }; diff --git a/src/convert/convertContext/convertContext.ts b/src/convert/convertContext/convertContext.ts index de219b4a89..4793e09071 100644 --- a/src/convert/convertContext/convertContext.ts +++ b/src/convert/convertContext/convertContext.ts @@ -9,7 +9,7 @@ import { RecompositionFinalizer } from './recompositionFinalizer'; import { NonDecompositionFinalizer } from './nonDecompositionFinalizer'; import { DecompositionFinalizer } from './decompositionFinalizer'; import { ConvertTransactionFinalizer } from './transactionFinalizer'; - +import { DecomposedLabelsFinalizer } from './decomposedLabelsFinalizer'; /** * A state manager over the course of a single metadata conversion call. */ @@ -17,6 +17,7 @@ export class ConvertContext { public readonly decomposition = new DecompositionFinalizer(); public readonly recomposition = new RecompositionFinalizer(); public readonly nonDecomposition = new NonDecompositionFinalizer(); + public readonly decomposedLabels = new DecomposedLabelsFinalizer(); // eslint-disable-next-line @typescript-eslint/require-await public async *executeFinalizers(defaultDirectory?: string): AsyncIterable { diff --git a/src/convert/convertContext/decomposedLabelsFinalizer.ts b/src/convert/convertContext/decomposedLabelsFinalizer.ts new file mode 100644 index 0000000000..5ebc998a28 --- /dev/null +++ b/src/convert/convertContext/decomposedLabelsFinalizer.ts @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { join } from 'node:path'; +import { ensure, JsonMap } from '@salesforce/ts-types'; +import type { CustomLabel } from '@jsforce/jsforce-node/lib/api/metadata'; +import { customLabelHasFullName } from '../../utils/metadata'; +import { MetadataType } from '../../registry'; +import { XML_NS_KEY, XML_NS_URL } from '../../common/constants'; +import { JsToXml } from '../streams'; +import { WriterFormat } from '../types'; +import { ConvertTransactionFinalizer } from './transactionFinalizer'; + +type CustomLabelState = { + /* + * Incoming child xml (CustomLabel) keyed by label fullname + */ + customLabelByFullName: Map; +}; + +/** + * Merges child components that share the same parent in the conversion pipeline + * into a single file. + * + * Inserts unclaimed child components into the parent that belongs to the default package + */ +export class DecomposedLabelsFinalizer extends ConvertTransactionFinalizer { + public transactionState: CustomLabelState = { + customLabelByFullName: new Map(), + }; + + /** to support custom presets (the only way this code should get hit at all pass in the type from a transformer that has registry access */ + public customLabelsType?: MetadataType; + + // have to maintain the existing interface + // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars + public async finalize(defaultDirectory?: string): Promise { + if (this.transactionState.customLabelByFullName.size === 0) { + return []; + } + return [ + { + component: { + type: ensure(this.customLabelsType, 'DecomposedCustomLabelsFinalizer should have set customLabelsType'), + fullName: 'CustomLabels', + }, + writeInfos: [ + { + output: join( + ensure(this.customLabelsType?.directoryName, 'directoryName missing from customLabels type'), + 'CustomLabels.labels' + ), + source: new JsToXml(generateXml(this.transactionState.customLabelByFullName)), + }, + ], + }, + ]; + } +} + +/** Return a json object that's built up from the mergeMap children */ +const generateXml = (children: Map): JsonMap => ({ + ['CustomLabels']: { + [XML_NS_KEY]: XML_NS_URL, + // for CustomLabels, that's `labels` + labels: Array.from(children.values()).filter(customLabelHasFullName).sort(sortLabelsByFullName), + }, +}); + +type CustomLabelWithFullName = CustomLabel & { fullName: string }; + +const sortLabelsByFullName = (a: CustomLabelWithFullName, b: CustomLabelWithFullName): number => + a.fullName.localeCompare(b.fullName); diff --git a/src/convert/convertContext/recompositionFinalizer.ts b/src/convert/convertContext/recompositionFinalizer.ts index 81e2cacc16..0b27140ca9 100644 --- a/src/convert/convertContext/recompositionFinalizer.ts +++ b/src/convert/convertContext/recompositionFinalizer.ts @@ -11,7 +11,6 @@ import { extractUniqueElementValue, getXmlElement, unwrapAndOmitNS } from '../.. import { MetadataComponent } from '../../resolve/types'; import { XML_NS_KEY, XML_NS_URL } from '../../common/constants'; import { ComponentSet } from '../../collections/componentSet'; -import { RecompositionStrategy } from '../../registry/types'; import { SourceComponent } from '../../resolve/sourceComponent'; import { JsToXml } from '../streams'; import { WriterFormat } from '../types'; @@ -127,7 +126,7 @@ const recompose = const getStartingXml = (cache: XmlCache) => async (parent: SourceComponent): Promise => - parent.type.strategies?.recomposition === RecompositionStrategy.StartEmpty + parent.type.strategies?.recomposition === 'startEmpty' ? {} : unwrapAndOmitNS(parent.type.name)(await getXmlFromCache(cache)(parent)) ?? {}; diff --git a/src/convert/streams.ts b/src/convert/streams.ts index 5fc18e1719..b1c2fa7e1c 100644 --- a/src/convert/streams.ts +++ b/src/convert/streams.ts @@ -72,11 +72,13 @@ export class ComponentConverter extends Transform { case 'source': if (mergeWith) { for (const mergeComponent of mergeWith) { - converts.push(transformer.toSourceFormat(chunk, mergeComponent)); + converts.push( + transformer.toSourceFormat({ component: chunk, mergeWith: mergeComponent, mergeSet: this.mergeSet }) + ); } } if (converts.length === 0) { - converts.push(transformer.toSourceFormat(chunk)); + converts.push(transformer.toSourceFormat({ component: chunk, mergeSet: this.mergeSet })); } break; case 'metadata': @@ -158,7 +160,13 @@ export class StandardWriter extends ComponentWriter { } // if there are children, resolve each file. o/w just pick one of the files to resolve - if (toResolve.size === 0 || chunk.component.type.children) { + // "resolve" means "make these show up in the FileResponses" + if ( + toResolve.size === 0 || + chunk.component.type.children !== undefined || + // make each decomposed label show up in the fileResponses + chunk.component.type.strategies?.transformer === 'decomposedLabels' + ) { // This is a workaround for a server side ListViews bug where // duplicate components are sent. W-9614275 if (toResolve.has(info.output)) { diff --git a/src/convert/transformers/baseMetadataTransformer.ts b/src/convert/transformers/baseMetadataTransformer.ts index f0c1666099..1b695269e9 100644 --- a/src/convert/transformers/baseMetadataTransformer.ts +++ b/src/convert/transformers/baseMetadataTransformer.ts @@ -20,5 +20,8 @@ export abstract class BaseMetadataTransformer implements MetadataTransformer { } public abstract toMetadataFormat(component: SourceComponent): Promise; - public abstract toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise; + public abstract toSourceFormat(input: { + component: SourceComponent; + mergeWith?: SourceComponent; + }): Promise; } diff --git a/src/convert/transformers/decomposeLabelsTransformer.ts b/src/convert/transformers/decomposeLabelsTransformer.ts new file mode 100644 index 0000000000..ed6079280e --- /dev/null +++ b/src/convert/transformers/decomposeLabelsTransformer.ts @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import type { CustomLabel } from '@jsforce/jsforce-node/lib/api/metadata'; +import { ensureArray } from '@salesforce/kit'; +import { customLabelHasFullName } from '../../utils/metadata'; +import { calculateRelativePath } from '../../utils/path'; +import { SourceComponent } from '../../resolve/sourceComponent'; +import { ToSourceFormatInput, WriteInfo } from '../types'; +import { JsToXml } from '../streams'; +import { unwrapAndOmitNS } from '../../utils/decomposed'; +import { DefaultMetadataTransformer } from './defaultMetadataTransformer'; + +/* Use for the metadata type CustomLabels */ +export class LabelsMetadataTransformer extends DefaultMetadataTransformer { + /** CustomLabels file => Array of CustomLabel WriteInfo (one for each label) */ + public async toSourceFormat({ component, mergeSet }: ToSourceFormatInput): Promise { + const labelType = this.registry.getTypeByName('CustomLabel'); + const partiallyAppliedPathCalculator = calculateRelativePath('source')({ + self: labelType, + }); + const xml = unwrapAndOmitNS('CustomLabels')(await component.parseXml()) as { labels: CustomLabel | CustomLabel[] }; + return ensureArray(xml.labels) // labels could parse to a single object and not an array if there's only 1 label + .filter(customLabelHasFullName) + .map((l) => ({ + // split each label into a separate label file + output: + // if present in the merge set, use that xml path, otherwise use the default path + mergeSet?.getComponentFilenamesByNameAndType({ fullName: l.fullName, type: labelType.name })?.[0] ?? + partiallyAppliedPathCalculator(l.fullName)(`${l.fullName}.label-meta.xml`), + source: new JsToXml({ CustomLabel: l }), + })); + } +} + +/* Use for the metadata type CustomLabel */ +export class LabelMetadataTransformer extends DefaultMetadataTransformer { + public async toMetadataFormat(component: SourceComponent): Promise { + // only need to do this once + this.context.decomposedLabels.customLabelsType ??= this.registry.getTypeByName('CustomLabels'); + this.context.decomposedLabels.transactionState.customLabelByFullName.set( + component.fullName, + unwrapAndOmitNS('CustomLabel')(await component.parseXml()) as CustomLabel + ); + return []; + } + + // toSourceFormat uses the default (merge them with the existing label) +} diff --git a/src/convert/transformers/decomposedMetadataTransformer.ts b/src/convert/transformers/decomposedMetadataTransformer.ts index fced068fe4..27eb82e629 100644 --- a/src/convert/transformers/decomposedMetadataTransformer.ts +++ b/src/convert/transformers/decomposedMetadataTransformer.ts @@ -14,10 +14,10 @@ import { calculateRelativePath } from '../../utils/path'; import { ForceIgnore } from '../../resolve/forceIgnore'; import { extractUniqueElementValue, objectHasSomeRealValues } from '../../utils/decomposed'; import type { MetadataComponent } from '../../resolve/types'; -import { DecompositionStrategy, type MetadataType } from '../../registry/types'; +import { type MetadataType } from '../../registry/types'; import { SourceComponent } from '../../resolve/sourceComponent'; import { JsToXml } from '../streams'; -import type { WriteInfo, XmlObj } from '../types'; +import type { ToSourceFormatInput, WriteInfo, XmlObj } from '../types'; import { META_XML_SUFFIX, XML_NS_KEY, XML_NS_URL } from '../../common/constants'; import type { SourcePath } from '../../common/types'; import { ComponentSet } from '../../collections/componentSet'; @@ -60,7 +60,7 @@ export class DecomposedMetadataTransformer extends BaseMetadataTransformer { return []; } - public async toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise { + public async toSourceFormat({ component, mergeWith }: ToSourceFormatInput): Promise { const forceIgnore = component.getForceIgnore(); // if the whole parent is ignored, we won't worry about decomposing things @@ -265,7 +265,7 @@ const getDefaultOutput = (component: MetadataComponent): SourcePath => { // there could be a '.' inside the child name (ex: PermissionSet.FieldPermissions.field uses Obj__c.Field__c) const childName = tail.length ? tail.join('.') : undefined; const output = join( - parent?.type.strategies?.decomposition === DecompositionStrategy.FolderPerType ? type.directoryName : '', + parent?.type.strategies?.decomposition === 'folderPerType' ? type.directoryName : '', `${childName ?? baseName}.${ensureString(component.type.suffix)}${META_XML_SUFFIX}` ); return join(calculateRelativePath('source')({ self: parent?.type ?? type })(fullName)(baseName), output); diff --git a/src/convert/transformers/defaultMetadataTransformer.ts b/src/convert/transformers/defaultMetadataTransformer.ts index 5865741596..8ca6b5a361 100644 --- a/src/convert/transformers/defaultMetadataTransformer.ts +++ b/src/convert/transformers/defaultMetadataTransformer.ts @@ -31,7 +31,13 @@ export class DefaultMetadataTransformer extends BaseMetadataTransformer { } // eslint-disable-next-line @typescript-eslint/require-await, class-methods-use-this - public async toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise { + public async toSourceFormat({ + component, + mergeWith, + }: { + component: SourceComponent; + mergeWith?: SourceComponent; + }): Promise { return getWriteInfos(component, 'source', mergeWith); } } diff --git a/src/convert/transformers/metadataTransformerFactory.ts b/src/convert/transformers/metadataTransformerFactory.ts index 311da4d478..d572a7d4e3 100644 --- a/src/convert/transformers/metadataTransformerFactory.ts +++ b/src/convert/transformers/metadataTransformerFactory.ts @@ -9,11 +9,11 @@ import { MetadataTransformer } from '../types'; import { SourceComponent } from '../../resolve/sourceComponent'; import { ConvertContext } from '../convertContext/convertContext'; import { RegistryAccess } from '../../registry/registryAccess'; -import { TransformerStrategy } from '../../registry/types'; import { DefaultMetadataTransformer } from './defaultMetadataTransformer'; import { DecomposedMetadataTransformer } from './decomposedMetadataTransformer'; import { StaticResourceMetadataTransformer } from './staticResourceMetadataTransformer'; import { NonDecomposedMetadataTransformer } from './nonDecomposedMetadataTransformer'; +import { LabelMetadataTransformer, LabelsMetadataTransformer } from './decomposeLabelsTransformer'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); @@ -32,15 +32,19 @@ export class MetadataTransformerFactory { const type = component.parent ? component.parent.type : component.type; const transformerId = type.strategies?.transformer; switch (transformerId) { - case TransformerStrategy.Standard: + case 'standard': case undefined: return new DefaultMetadataTransformer(this.registry, this.context); - case TransformerStrategy.Decomposed: + case 'decomposed': return new DecomposedMetadataTransformer(this.registry, this.context); - case TransformerStrategy.StaticResource: + case 'staticResource': return new StaticResourceMetadataTransformer(this.registry, this.context); - case TransformerStrategy.NonDecomposed: + case 'nonDecomposed': return new NonDecomposedMetadataTransformer(this.registry, this.context); + case 'decomposedLabels': + return component.type.name === 'CustomLabels' + ? new LabelsMetadataTransformer(this.registry, this.context) + : new LabelMetadataTransformer(this.registry, this.context); default: throw messages.createError('error_missing_transformer', [type.name, transformerId]); } diff --git a/src/convert/transformers/nonDecomposedMetadataTransformer.ts b/src/convert/transformers/nonDecomposedMetadataTransformer.ts index 94c53ef60e..6112399f7d 100644 --- a/src/convert/transformers/nonDecomposedMetadataTransformer.ts +++ b/src/convert/transformers/nonDecomposedMetadataTransformer.ts @@ -8,8 +8,7 @@ import { get, getString, JsonMap } from '@salesforce/ts-types'; import { ensureArray } from '@salesforce/kit'; import { Messages } from '@salesforce/core'; -import { WriteInfo } from '../types'; -import { SourceComponent } from '../../resolve/sourceComponent'; +import { ToSourceFormatInput, WriteInfo } from '../types'; import { DecomposedMetadataTransformer } from './decomposedMetadataTransformer'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); @@ -22,7 +21,7 @@ const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sd export class NonDecomposedMetadataTransformer extends DecomposedMetadataTransformer { // streams uses mergeWith for all types. Removing it would break the interface // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise { + public async toSourceFormat({ component, mergeWith }: ToSourceFormatInput): Promise { // this will only include the incoming (retrieved) labels, not the local file const parentXml = await component.parseXml(); const xmlPathToChildren = `${component.type.name}.${component.type.directoryName}`; diff --git a/src/convert/transformers/staticResourceMetadataTransformer.ts b/src/convert/transformers/staticResourceMetadataTransformer.ts index 668a1eeb70..d5ca4b537e 100644 --- a/src/convert/transformers/staticResourceMetadataTransformer.ts +++ b/src/convert/transformers/staticResourceMetadataTransformer.ts @@ -13,7 +13,7 @@ import { createWriteStream } from 'graceful-fs'; import { Logger, Messages, SfError } from '@salesforce/core'; import { isEmpty } from '@salesforce/kit'; import { baseName } from '../../utils/path'; -import { WriteInfo } from '../types'; +import { ToSourceFormatInput, WriteInfo } from '../types'; import { SourceComponent } from '../../resolve/sourceComponent'; import { SourcePath } from '../../common/types'; import { ensureFileExists } from '../../utils/fileSystemHandler'; @@ -97,7 +97,7 @@ export class StaticResourceMetadataTransformer extends BaseMetadataTransformer { ]; } - public async toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise { + public async toSourceFormat({ component, mergeWith }: ToSourceFormatInput): Promise { const { xml, content } = component; if (!content) { diff --git a/src/convert/types.ts b/src/convert/types.ts index e0e790a5bf..36f30d69e1 100644 --- a/src/convert/types.ts +++ b/src/convert/types.ts @@ -6,6 +6,7 @@ */ import { Readable } from 'node:stream'; import { JsonMap } from '@salesforce/ts-types'; +import { ComponentSet } from '../collections/componentSet'; import { XML_NS_KEY, XML_NS_URL } from '../common/constants'; import { FileResponseSuccess } from '../client/types'; import { SourcePath } from '../common/types'; @@ -74,13 +75,19 @@ export type MergeConfig = { forceIgnoredPaths?: Set; }; +export type ToSourceFormatInput = { + component: SourceComponent; + mergeWith?: SourceComponent; + mergeSet?: ComponentSet; +}; +export type ToSourceFormat = (input: ToSourceFormatInput) => Promise; /** * Transforms metadata component files into different SFDX file formats */ export type MetadataTransformer = { defaultDirectory?: string; + toSourceFormat: ToSourceFormat; toMetadataFormat(component: SourceComponent): Promise; - toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise; }; // -------------- diff --git a/src/index.ts b/src/index.ts index 550b1ef50b..2e6e92ac42 100644 --- a/src/index.ts +++ b/src/index.ts @@ -90,15 +90,10 @@ export { FromSourceOptions, FromManifestOptions, } from './collections'; -export { - RegistryAccess, - registry, - getCurrentApiVersion, - MetadataRegistry, - MetadataType, - DecompositionStrategy, - RecompositionStrategy, - TransformerStrategy, -} from './registry'; + +export { RegistryAccess, registry, getCurrentApiVersion, MetadataRegistry, MetadataType } from './registry'; + +// TODO: don't export these strategies +export { DecompositionStrategy, TransformerStrategy, RecompositionStrategy } from './registry/types'; export { presetMap } from './registry/presets/presetMap'; diff --git a/src/registry/index.ts b/src/registry/index.ts index f214188211..d1f892d998 100644 --- a/src/registry/index.ts +++ b/src/registry/index.ts @@ -8,10 +8,4 @@ export { registry } from './registry'; export { standardValueSet } from './standardvalueset'; export { RegistryAccess } from './registryAccess'; export { getCurrentApiVersion } from './coverage'; -export { - MetadataRegistry, - MetadataType, - DecompositionStrategy, - RecompositionStrategy, - TransformerStrategy, -} from './types'; +export { MetadataRegistry, MetadataType } from './types'; diff --git a/src/registry/presets/decomposeCustomLabelsBeta.json b/src/registry/presets/decomposeCustomLabelsBeta.json index 4d121d4e63..591d87c7d4 100644 --- a/src/registry/presets/decomposeCustomLabelsBeta.json +++ b/src/registry/presets/decomposeCustomLabelsBeta.json @@ -2,12 +2,12 @@ "childTypes": { "customlabel": "customlabels" }, - "suffixes": { - "label": "customlabel" - }, "strictDirectoryNames": { "labels": "customlabels" }, + "suffixes": { + "label": "customlabel" + }, "types": { "customlabels": { "children": { @@ -18,16 +18,15 @@ "customlabel": { "directoryName": "test", "id": "customlabel", + "isAddressable": false, "name": "CustomLabel", "suffix": "label", - "isAddressable": false, "supportsWildcardAndName": true, "uniqueIdElement": "fullName", "xmlElementName": "labels" } } }, - "strictDirectoryName": true, "directoryName": "labels", "id": "customlabels", "ignoreParsedFullName": false, @@ -37,6 +36,7 @@ "decomposition": "topLevel", "transformer": "decomposed" }, + "strictDirectoryName": true, "suffix": "labels", "supportsPartialDelete": true } diff --git a/src/registry/presets/decomposeCustomLabelsBeta2.json b/src/registry/presets/decomposeCustomLabelsBeta2.json new file mode 100644 index 0000000000..ea65af0b74 --- /dev/null +++ b/src/registry/presets/decomposeCustomLabelsBeta2.json @@ -0,0 +1,32 @@ +{ + "childTypes": { + "customlabel": "" + }, + "strictDirectoryNames": {}, + "suffixes": { + "label": "customlabel", + "labels": "customlabels" + }, + "types": { + "customlabel": { + "directoryName": "labels", + "id": "customlabel", + "name": "CustomLabel", + "strategies": { + "adapter": "default", + "transformer": "decomposedLabels" + }, + "suffix": "label" + }, + "customlabels": { + "directoryName": "labels", + "id": "customlabels", + "name": "CustomLabels", + "strategies": { + "adapter": "default", + "transformer": "decomposedLabels" + }, + "suffix": "labels" + } + } +} diff --git a/src/registry/presets/presetMap.ts b/src/registry/presets/presetMap.ts index 2a0353bb68..1cb0ea73b6 100644 --- a/src/registry/presets/presetMap.ts +++ b/src/registry/presets/presetMap.ts @@ -8,11 +8,13 @@ import { MetadataRegistry } from '../types'; // we have to import all presets explicitly for VSCE's esbuild bundling process import * as decomposeCustomLabelsBeta from './decomposeCustomLabelsBeta.json'; +import * as decomposeCustomLabelsBeta2 from './decomposeCustomLabelsBeta2.json'; import * as decomposePermissionSetBeta from './decomposePermissionSetBeta.json'; import * as decomposeSharingRulesBeta from './decomposeSharingRulesBeta.json'; import * as decomposeWorkflowBeta from './decomposeWorkflowBeta.json'; export const presetMap = new Map([ + ['decomposeCustomLabelsBeta2', decomposeCustomLabelsBeta2 as MetadataRegistry], ['decomposeCustomLabelsBeta', decomposeCustomLabelsBeta as MetadataRegistry], ['decomposePermissionSetBeta', decomposePermissionSetBeta as MetadataRegistry], ['decomposeSharingRulesBeta', decomposeSharingRulesBeta as MetadataRegistry], diff --git a/src/registry/types.ts b/src/registry/types.ts index 034b7ce9ca..a0c4bb8710 100644 --- a/src/registry/types.ts +++ b/src/registry/types.ts @@ -140,7 +140,7 @@ export type MetadataType = { */ strategies?: { adapter: 'mixedContent' | 'matchingContentFile' | 'decomposed' | 'digitalExperience' | 'bundle' | 'default'; - transformer?: 'decomposed' | 'staticResource' | 'nonDecomposed' | 'standard'; + transformer?: 'decomposed' | 'staticResource' | 'nonDecomposed' | 'standard' | 'decomposedLabels'; decomposition?: 'topLevel' | 'folderPerType'; recomposition?: 'startEmpty'; }; @@ -168,6 +168,7 @@ type DirectoryIndex = { }; /** + * @deprecated. See the strategies union type on the registry types for the valid names * Strategy names for handling component decomposition. */ export const enum DecompositionStrategy { @@ -182,6 +183,7 @@ export const enum DecompositionStrategy { } /** + * @deprecated. See the strategies union type on the registry types for the valid names * Strategy names for handling component recomposition. */ export const enum RecompositionStrategy { @@ -192,6 +194,7 @@ export const enum RecompositionStrategy { } /** + * @deprecated. See the strategies union type on the registry types for the valid names * Strategy names for the type of transformation to use for metadata types. */ export const enum TransformerStrategy { @@ -199,6 +202,7 @@ export const enum TransformerStrategy { Decomposed = 'decomposed', StaticResource = 'staticResource', NonDecomposed = 'nonDecomposed', + DecomposedLabels = 'decomposedLabels', } type Channel = { diff --git a/src/registry/variants.ts b/src/registry/variants.ts index 78532d9b4a..7846cc81b7 100644 --- a/src/registry/variants.ts +++ b/src/registry/variants.ts @@ -10,60 +10,72 @@ import { MetadataRegistry } from './types'; import * as registryData from './metadataRegistry.json'; import { presetMap } from './presets/presetMap'; -export type RegistryLoadInput = { - /** The project directory to look at sfdx-project.json file - * will default to the current working directory - * if no project file is found, the standard registry will be returned without modifications - */ - projectDir?: string; +type ProjectVariants = { + registryCustomizations?: MetadataRegistry; + presets?: MetadataRegistry[]; + projectDir?: never; }; +export type RegistryLoadInput = + | { + /** The project directory to look at sfdx-project.json file + * will default to the current working directory + * if no project file is found, the standard registry will be returned without modifications + */ + projectDir?: string; + registryCustomizations?: never; + presets?: never; + } + | ProjectVariants; + /** combine the standard registration with any overrides specific in the sfdx-project.json */ export const getEffectiveRegistry = (input?: RegistryLoadInput): MetadataRegistry => - deepFreeze(firstLevelMerge(registryData as MetadataRegistry, loadVariants(input))); + deepFreeze( + removeEmptyStrings( + firstLevelMerge( + registryData as MetadataRegistry, + mergeVariants( + input?.presets?.length ?? input?.registryCustomizations ? input : getProjectVariants(input?.projectDir) + ) + ) + ) + ); /** read the project to get additional registry customizations and sourceBehaviorOptions */ -const loadVariants = ({ projectDir }: RegistryLoadInput = {}): MetadataRegistry => { - const logger = Logger.childFromRoot('variants'); +const getProjectVariants = (projectDir?: string): ProjectVariants => { + const logger = Logger.childFromRoot('variants:getProjectVariants'); const projJson = maybeGetProject(projectDir); if (!projJson) { logger.debug('no project found, using standard registry'); // there might not be a project at all and that's ok - return emptyRegistry; + return {}; } // there might not be any customizations in a project, so we default to the emptyRegistry - const customizations = projJson.get('registryCustomizations') ?? emptyRegistry; - const sourceBehaviorOptions = [ + const registryCustomizations = projJson.get('registryCustomizations') ?? emptyRegistry; + const presets = [ ...new Set([ // TODO: deprecated, remove this ...(projJson.get('registryPresets') ?? []), ...(projJson.get('sourceBehaviorOptions') ?? []), ]), ]; - if (Object.keys(customizations.types).length > 0) { - logger.debug( - `found registryCustomizations for types [${Object.keys(customizations.types).join(',')}] in ${projJson.getPath()}` - ); - } - if (sourceBehaviorOptions.length > 0) { - logger.debug(`using sourceBehaviorOptions [${sourceBehaviorOptions.join(',')}] in ${projJson.getPath()}`); - } - const registryFromPresets = sourceBehaviorOptions.reduce( - (prev, curr) => firstLevelMerge(prev, loadPreset(curr)), + return logProjectVariants( + { + registryCustomizations, + presets: presets.map(loadPreset), + }, + projJson.getPath() + ); +}; + +const mergeVariants = ({ registryCustomizations = emptyRegistry, presets }: ProjectVariants): MetadataRegistry => { + const registryFromPresets = [...(presets ?? []), registryCustomizations].reduce( + (prev, curr) => firstLevelMerge(prev, curr), emptyRegistry ); - if (sourceBehaviorOptions.length > 0 || Object.keys(customizations.types).length > 0) { - void Lifecycle.getInstance().emitTelemetry({ - library: 'SDR', - eventName: 'RegistryVariants', - presetCount: sourceBehaviorOptions.length, - presets: sourceBehaviorOptions.join(','), - customizationsCount: Object.keys(customizations.types).length, - customizationsTypes: Object.keys(customizations.types).join(','), - }); - } - return firstLevelMerge(registryFromPresets, customizations); + + return firstLevelMerge(registryFromPresets, registryCustomizations); }; const maybeGetProject = (projectDir?: string): SfProjectJson | undefined => { @@ -94,12 +106,51 @@ const emptyRegistry = { childTypes: {}, suffixes: {}, strictDirectoryNames: {}, -} satisfies MetadataRegistry; +} as const satisfies MetadataRegistry; /** merge the children of the top-level properties (ex: types, suffixes, etc) on 2 registries */ export const firstLevelMerge = (original: MetadataRegistry, overrides: MetadataRegistry): MetadataRegistry => ({ types: { ...original.types, ...(overrides.types ?? {}) }, childTypes: { ...original.childTypes, ...(overrides.childTypes ?? {}) }, suffixes: { ...original.suffixes, ...(overrides.suffixes ?? {}) }, - strictDirectoryNames: { ...original.strictDirectoryNames, ...(overrides.strictDirectoryNames ?? {}) }, + strictDirectoryNames: { + ...original.strictDirectoryNames, + ...(overrides.strictDirectoryNames ?? {}), + }, }); + +const removeEmptyStrings = (reg: MetadataRegistry): MetadataRegistry => ({ + types: reg.types, + childTypes: removeEmptyString(reg.childTypes), + suffixes: removeEmptyString(reg.suffixes), + strictDirectoryNames: removeEmptyString(reg.strictDirectoryNames), +}); + +// presets can remove an entry by setting it to an empty string ex: { "childTypes": { "foo": "" } } +const removeEmptyString = (obj: Record): Record => + Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== '')); + +// returns the projectVariants passed in. Side effects: logger and telemetry only +const logProjectVariants = (variants: ProjectVariants, projectDir: string): ProjectVariants => { + const customizationTypes = Object.keys(variants.registryCustomizations?.types ?? {}); + const logger = Logger.childFromRoot('variants:logProjectVariants'); + if (customizationTypes.length) { + logger.debug(`found registryCustomizations for types [${customizationTypes.join(',')}] in ${projectDir}`); + } + if (variants.presets?.length) { + logger.debug(`using sourceBehaviorOptions [${variants.presets.join(',')}] in ${projectDir}`); + } + if (variants?.presets?.length ?? customizationTypes.length) { + void Lifecycle.getInstance().emitTelemetry({ + library: 'SDR', + eventName: 'RegistryVariants', + presetCount: variants.presets?.length ?? 0, + presets: variants.presets?.join(','), + customizationsCount: customizationTypes.length, + customizationsTypes: customizationTypes.join(','), + }); + } else { + logger.debug(`no registryCustomizations or sourceBehaviorOptions found in ${projectDir}`); + } + return variants; +}; diff --git a/src/resolve/sourceComponent.ts b/src/resolve/sourceComponent.ts index 2ec114d8e0..d23bccc243 100644 --- a/src/resolve/sourceComponent.ts +++ b/src/resolve/sourceComponent.ts @@ -9,10 +9,10 @@ import { SfError } from '@salesforce/core/sfError'; import { Messages } from '@salesforce/core/messages'; import { Lifecycle } from '@salesforce/core/lifecycle'; -import { XMLParser, XMLValidator } from 'fast-xml-parser'; +import { XMLValidator } from 'fast-xml-parser'; import { get, getString, JsonMap } from '@salesforce/ts-types'; import { ensureArray } from '@salesforce/kit'; -import { XML_COMMENT_PROP_NAME } from '../common/constants'; +import { parser } from '../utils/metadata'; import { getXmlElement } from '../utils/decomposed'; import { baseName, baseWithoutSuffixes, parseMetadataXml, calculateRelativePath } from '../utils/path'; import { replacementIterations } from '../convert/replacements'; @@ -284,16 +284,6 @@ export class SourceComponent implements MetadataComponent { } private parse(contents: string): T { - // include tag attributes and don't parse text node as number - const parser = new XMLParser({ - ignoreAttributes: false, - parseTagValue: false, - parseAttributeValue: false, - cdataPropName: '__cdata', - ignoreDeclaration: true, - numberParseOptions: { leadingZeros: false, hex: false }, - commentPropName: XML_COMMENT_PROP_NAME, - }); const parsed = parser.parse(String(contents)) as T; const [firstElement] = Object.keys(parsed); if (firstElement === this.type.name) { diff --git a/src/utils/filePathGenerator.ts b/src/utils/filePathGenerator.ts index b133a0a73a..273720fdb0 100644 --- a/src/utils/filePathGenerator.ts +++ b/src/utils/filePathGenerator.ts @@ -10,7 +10,6 @@ import { isPlainObject } from '@salesforce/ts-types'; import { MetadataComponent } from '../resolve/types'; import { META_XML_SUFFIX } from '../common/constants'; import { RegistryAccess } from '../registry/registryAccess'; -import { registry } from '../registry/registry'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); @@ -65,7 +64,7 @@ export const filePathsFromMetadataComponent = ( } // this needs to be done before the other types because of potential overlaps - if (!type.children && Object.keys(registry.childTypes).includes(type.id)) { + if (!type.children && Object.keys(registryAccess.getRegistry().childTypes).includes(type.id)) { return getDecomposedChildType({ fullName, type }, packageDir); } @@ -75,7 +74,7 @@ export const filePathsFromMetadataComponent = ( } // basic metadata (with or without folders) - if (!type.children && !type.strategies && type.suffix) { + if (!type.children && type.suffix && (!type.strategies || type.strategies.transformer === 'decomposedLabels')) { return (type.inFolder ?? type.folderType ? generateFolders({ fullName, type }, packageDirWithTypeDir) : []).concat([ join(packageDirWithTypeDir, `${fullName}.${type.suffix}${META_XML_SUFFIX}`), ]); diff --git a/src/utils/metadata.ts b/src/utils/metadata.ts index ad7cd4f92b..d09c72aadc 100644 --- a/src/utils/metadata.ts +++ b/src/utils/metadata.ts @@ -5,7 +5,21 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { META_XML_SUFFIX } from '../common'; +import type { CustomLabel } from '@jsforce/jsforce-node/lib/api/metadata'; +import { SfError } from '@salesforce/core'; +import { XMLParser } from 'fast-xml-parser'; +import { META_XML_SUFFIX, XML_COMMENT_PROP_NAME } from '../common/constants'; + +export const parser = new XMLParser({ + // include tag attributes and don't parse text node as number + ignoreAttributes: false, + parseTagValue: false, + parseAttributeValue: false, + cdataPropName: '__cdata', + ignoreDeclaration: true, + numberParseOptions: { leadingZeros: false, hex: false }, + commentPropName: XML_COMMENT_PROP_NAME, +}); export function generateMetaXML(typeName: string, apiVersion: string, status: string): string { let templateResult = '\n'; @@ -25,3 +39,10 @@ export function generateMetaXMLPath(sourcePath: string): string { export function trimMetaXmlSuffix(sourcePath: string): string { return sourcePath.endsWith(META_XML_SUFFIX) ? sourcePath.replace(META_XML_SUFFIX, '') : sourcePath; } + +export const customLabelHasFullName = (label: CustomLabel): label is CustomLabel & { fullName: string } => { + if (label.fullName === undefined) { + throw SfError.create({ message: 'Label does not have a fullName', data: label }); + } + return true; +}; diff --git a/test/convert/streams.test.ts b/test/convert/streams.test.ts index c93067b8c8..dc2d07e876 100644 --- a/test/convert/streams.test.ts +++ b/test/convert/streams.test.ts @@ -12,6 +12,7 @@ import { Logger, SfError, Messages } from '@salesforce/core'; import { expect, assert } from 'chai'; import { createSandbox, SinonStub } from 'sinon'; import JSZip from 'jszip'; +import { ToSourceFormatInput } from '../../src/convert/types'; import * as streams from '../../src/convert/streams'; import * as fsUtil from '../../src/utils/fileSystemHandler'; import { ComponentSet, MetadataResolver, RegistryAccess, SourceComponent, WriteInfo, WriterFormat } from '../../src'; @@ -35,7 +36,7 @@ class TestTransformer extends BaseMetadataTransformer { } // partial implementation only for tests // eslint-disable-next-line class-methods-use-this, @typescript-eslint/require-await - public async toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise { + public async toSourceFormat({ mergeWith }: ToSourceFormatInput): Promise { const output = mergeWith ? mergeWith.content ?? mergeWith.xml : '/type/file.s'; assert(output); return [{ output, source: new Readable() }]; @@ -106,7 +107,7 @@ describe('Streams', () => { expect(err).to.be.undefined; expect(data).to.deep.equal({ component, - writeInfos: await transformer.toSourceFormat(component), + writeInfos: await transformer.toSourceFormat({ component }), }); done(); } catch (e) { @@ -133,7 +134,7 @@ describe('Streams', () => { expect(err).to.be.undefined; expect(data).to.deep.equal({ component: newComponent, - writeInfos: await transformer.toSourceFormat(newComponent, component), + writeInfos: await transformer.toSourceFormat({ component: newComponent, mergeWith: component }), }); done(); } catch (e) { @@ -162,8 +163,8 @@ describe('Streams', () => { expect(err).to.be.undefined; expect(data).to.deep.equal({ component: newComponent, - writeInfos: (await transformer.toSourceFormat(newComponent, component)).concat( - await transformer.toSourceFormat(newComponent, secondMergeComponent) + writeInfos: (await transformer.toSourceFormat({ component: newComponent, mergeWith: component })).concat( + await transformer.toSourceFormat({ component: newComponent, mergeWith: secondMergeComponent }) ), }); done(); diff --git a/test/convert/transformers/decomposedLabelsTransformer.test.ts b/test/convert/transformers/decomposedLabelsTransformer.test.ts new file mode 100644 index 0000000000..0590e11240 --- /dev/null +++ b/test/convert/transformers/decomposedLabelsTransformer.test.ts @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { join } from 'node:path'; +import { expect, assert } from 'chai'; +import { parser } from '../../../src/utils/metadata'; +import { stream2buffer } from '../../../src/convert/streams'; +import { DecomposedLabelsFinalizer } from '../../../src/convert/convertContext/decomposedLabelsFinalizer'; +import { ComponentSet } from '../../../src/collections/componentSet'; +import { RegistryAccess } from '../../../src/registry/registryAccess'; +import { + EMPTY_CUSTOM_LABELS_CMP, + ONE_CUSTOM_LABELS_CMP, + ONLY_LABEL_CMP_IN_ANOTHER_DIR_CMP, + ONLY_LABEL_CMP_IN_DEFAULT_DIR_CMP, + ONLY_LABEL_NO_DIR_CMP, + OTHER_LABEL_CMP, + THREE_CUSTOM_LABELS_CMP, +} from '../../mock/type-constants/decomposedCustomLabelsConstant'; +import { + LabelMetadataTransformer, + LabelsMetadataTransformer, +} from '../../../src/convert/transformers/decomposeLabelsTransformer'; +import { getEffectiveRegistry } from '../../../src/registry/variants'; +import { presetMap } from '../../../src/registry/presets/presetMap'; + +describe('DecomposedCustomLabelTransformer', () => { + const regAcc = new RegistryAccess(getEffectiveRegistry({ presets: [presetMap.get('decomposeCustomLabelsBeta2')!] })); + + describe('LabelsMetadataTransformer', () => { + describe('toSourceFormat', () => { + describe('WriteInfo output (where the file will write to)', () => { + describe('default dir', () => { + it('multiple labels in a single customLabels', async () => { + const component = THREE_CUSTOM_LABELS_CMP; + const xf = new LabelsMetadataTransformer(regAcc); + const result = await xf.toSourceFormat({ component }); + expect(result).to.have.length(3); + result.map((l) => { + expect(l.output).to.include(join('main', 'default', 'labels')); + }); + expect(result[0].output).to.match(/DeleteMe.label-meta.xml$/); + expect(result[1].output).to.match(/KeepMe1.label-meta.xml$/); + expect(result[2].output).to.match(/KeepMe2.label-meta.xml$/); + }); + it('single label in customLabels', async () => { + const component = ONE_CUSTOM_LABELS_CMP; + const xf = new LabelsMetadataTransformer(regAcc); + const result = await xf.toSourceFormat({ component }); + expect(result).to.have.length(1); + expect(result[0].output).to.equal(join('main', 'default', 'labels', 'OnlyLabel.label-meta.xml')); + }); + it('empty customLabels ', async () => { + const component = EMPTY_CUSTOM_LABELS_CMP; + const xf = new LabelsMetadataTransformer(regAcc); + const result = await xf.toSourceFormat({ component }); + expect(result).to.deep.equal([]); + }); + it('merge component in defaultDir', async () => { + const component = ONE_CUSTOM_LABELS_CMP; + const xf = new LabelsMetadataTransformer(regAcc); + const result = await xf.toSourceFormat({ + component, + mergeSet: new ComponentSet([ONLY_LABEL_CMP_IN_DEFAULT_DIR_CMP], regAcc), + }); + expect(result).to.have.length(1); + expect(result[0].output).to.equal(ONLY_LABEL_CMP_IN_DEFAULT_DIR_CMP.xml!); + }); + }); + }); + describe('no labels dir', () => { + it('merge matches label not in a labels dir', async () => { + const component = ONE_CUSTOM_LABELS_CMP; + const xf = new LabelsMetadataTransformer(regAcc); + const result = await xf.toSourceFormat({ + component, + mergeSet: new ComponentSet([ONLY_LABEL_NO_DIR_CMP], regAcc), + }); + expect(result).to.have.length(1); + expect(result[0].output).to.equal(ONLY_LABEL_NO_DIR_CMP.xml!); + }); + }); + describe('non-default dir', () => { + it('merge component in nonDefault dir => matches the original location', async () => { + const component = ONE_CUSTOM_LABELS_CMP; + const xf = new LabelsMetadataTransformer(regAcc); + const result = await xf.toSourceFormat({ + component, + mergeSet: new ComponentSet([ONLY_LABEL_CMP_IN_ANOTHER_DIR_CMP], regAcc), + }); + expect(result).to.have.length(1); + expect(result[0].output).to.equal(ONLY_LABEL_CMP_IN_ANOTHER_DIR_CMP.xml!); + }); + it('merge component in nonDefault dir, but mdapi does not match existing source ', async () => { + const component = THREE_CUSTOM_LABELS_CMP; + const xf = new LabelsMetadataTransformer(regAcc); + const result = await xf.toSourceFormat({ + component, + mergeSet: new ComponentSet([ONLY_LABEL_CMP_IN_ANOTHER_DIR_CMP], regAcc), + }); + expect(result).to.have.length(3); + result.map((l) => { + expect(l.output).to.include(join('main', 'default', 'labels')); + }); + }); + }); + }); + }); + + describe('LabelMetadataTransformer', () => { + describe('toMetadataFormat', () => { + it('should set the customLabelsType in the context', async () => { + const component = ONLY_LABEL_CMP_IN_DEFAULT_DIR_CMP; + const xf = new LabelMetadataTransformer(regAcc); + const result = await xf.toMetadataFormat(component); + expect(result).to.deep.equal([]); + expect(xf.context.decomposedLabels.customLabelsType).to.equal(regAcc.getTypeByName('CustomLabels')); + }); + it('should put the entire CustomLabel xml content into the transactionState', async () => { + const component = ONLY_LABEL_CMP_IN_DEFAULT_DIR_CMP; + const xf = new LabelMetadataTransformer(regAcc); + const result = await xf.toMetadataFormat(component); + expect(result).to.deep.equal([]); + expect(xf.context.decomposedLabels.transactionState.customLabelByFullName.size).to.equal(1); + const stateEntry = xf.context.decomposedLabels.transactionState.customLabelByFullName.get( + ONLY_LABEL_CMP_IN_DEFAULT_DIR_CMP.fullName + ); + // component properties are in the state. We could say the same about the rest of CustomLabel properties + // but would have to the xml => js and omitNS stuff here to compare + expect(stateEntry?.fullName).to.deep.equal(component.fullName); + }); + }); + describe('finalizer', () => { + it('single label from source to mdapi', async () => { + const component = ONLY_LABEL_CMP_IN_DEFAULT_DIR_CMP; + const xf = new LabelMetadataTransformer(regAcc); + await xf.toMetadataFormat(component); + const finalizer = new DecomposedLabelsFinalizer(); + finalizer.customLabelsType = regAcc.getTypeByName('CustomLabels'); + finalizer.transactionState = xf.context.decomposedLabels.transactionState; + const result = await finalizer.finalize(); + expect(result).to.have.length(1); + expect(result[0].component.fullName).to.equal('CustomLabels'); + expect(result[0].component.type.name).to.equal('CustomLabels'); + expect(result[0].writeInfos).to.have.length(1); + assert(result[0].writeInfos[0].source); + const contents = (await stream2buffer(result[0].writeInfos[0].source)).toString(); + expect(parser.parse(contents)).to.deep.equal(await ONE_CUSTOM_LABELS_CMP.parseXml()); + }); + it('2 labels from source to mdapi', async () => { + const component1 = ONLY_LABEL_CMP_IN_DEFAULT_DIR_CMP; + const component2 = OTHER_LABEL_CMP; + const xf = new LabelMetadataTransformer(regAcc); + await xf.toMetadataFormat(component1); + await xf.toMetadataFormat(component2); + const finalizer = new DecomposedLabelsFinalizer(); + finalizer.customLabelsType = regAcc.getTypeByName('CustomLabels'); + finalizer.transactionState = xf.context.decomposedLabels.transactionState; + const result = await finalizer.finalize(); + expect(result).to.have.length(1); + expect(result[0].component.fullName).to.equal('CustomLabels'); + expect(result[0].component.type.name).to.equal('CustomLabels'); + // still produces only 1 writeInfo + expect(result[0].writeInfos).to.have.length(1); + assert(result[0].writeInfos[0].source); + const contents = (await stream2buffer(result[0].writeInfos[0].source)).toString(); + // with 2 labels in it + expect(parser.parse(contents).CustomLabels.labels).to.have.length(2); + }); + }); + }); +}); diff --git a/test/convert/transformers/decomposedMetadataTransformer.test.ts b/test/convert/transformers/decomposedMetadataTransformer.test.ts index 1d48d4bfc1..980e2ca4e6 100644 --- a/test/convert/transformers/decomposedMetadataTransformer.test.ts +++ b/test/convert/transformers/decomposedMetadataTransformer.test.ts @@ -132,7 +132,7 @@ describe('DecomposedMetadataTransformer', () => { }, }); - const result = await transformer.toSourceFormat(component); + const result = await transformer.toSourceFormat({ component }); expect(context.decomposition.transactionState.size).to.equal(0); expect(result).to.deep.equal([ @@ -193,7 +193,7 @@ describe('DecomposedMetadataTransformer', () => { ) .returns(false); - const result = await transformer.toSourceFormat(component); + const result = await transformer.toSourceFormat({ component }); expect(context.decomposition.transactionState.size).to.equal(0); expect(result).to.deep.equal([ @@ -239,7 +239,7 @@ describe('DecomposedMetadataTransformer', () => { }, }); - const result = await transformer.toSourceFormat(component); + const result = await transformer.toSourceFormat({ component }); expect(result).to.deep.equal([ { @@ -277,7 +277,7 @@ describe('DecomposedMetadataTransformer', () => { }, }); - const result = await transformer.toSourceFormat(component); + const result = await transformer.toSourceFormat({ component }); expect(result).to.deep.equal([ { @@ -327,7 +327,7 @@ describe('DecomposedMetadataTransformer', () => { }, }); - const result = await transformer.toSourceFormat(component); + const result = await transformer.toSourceFormat({ component }); expect(result).to.deep.equal([ { @@ -378,7 +378,7 @@ describe('DecomposedMetadataTransformer', () => { }, }); - const result = await transformer.toSourceFormat(cft, cot); + const result = await transformer.toSourceFormat({ component: cft, mergeWith: cot }); expect(result).to.deep.equal([ { @@ -417,7 +417,7 @@ describe('DecomposedMetadataTransformer', () => { const transformer = new DecomposedMetadataTransformer(); $$.SANDBOX.stub(component, 'parseXml').resolves({}); - const result = await transformer.toSourceFormat(component); + const result = await transformer.toSourceFormat({ component }); expect(result).to.be.an('array').with.lengthOf(1); // there will be a file produced, with just the outer type (ex: CustomObject) and the xmlns declaration @@ -446,7 +446,7 @@ describe('DecomposedMetadataTransformer', () => { .withArgs(join('main', 'default', 'objects', 'customObject__c', 'customObject__c.object-meta.xml')) .returns(true); - const result = await transformer.toSourceFormat(component); + const result = await transformer.toSourceFormat({ component }); expect(result).to.deep.equal([]); }); @@ -472,7 +472,7 @@ describe('DecomposedMetadataTransformer', () => { }); const transformer = new DecomposedMetadataTransformer(registryAccess); - const result = await transformer.toSourceFormat(componentToConvert, component); + const result = await transformer.toSourceFormat({ component: componentToConvert, mergeWith: component }); expect(result).to.deep.equal([ { @@ -509,7 +509,7 @@ describe('DecomposedMetadataTransformer', () => { }); const transformer = new DecomposedMetadataTransformer(); - const result = await transformer.toSourceFormat(componentToConvert, component); + const result = await transformer.toSourceFormat({ component: componentToConvert, mergeWith: component }); expect(result).to.deep.equal([ { @@ -556,7 +556,7 @@ describe('DecomposedMetadataTransformer', () => { try { // NOTE: it doesn't matter what the first component is for this test since it's all // about the child components of the parentComponent. - await transformer.toSourceFormat(component, parentComponent); + await transformer.toSourceFormat({ component, mergeWith: parentComponent }); assert(false, 'expected TypeInferenceError to be thrown'); } catch (err) { assert(err instanceof Error); @@ -590,7 +590,7 @@ describe('DecomposedMetadataTransformer', () => { const context = new ConvertContext(); const transformer = new DecomposedMetadataTransformer(registryAccess, context); - const result = await transformer.toSourceFormat(component, componentToMerge); + const result = await transformer.toSourceFormat({ component, mergeWith: componentToMerge }); expect(result).to.be.empty; expect( context.decomposition.transactionState.get(`${mergeComponentChild.type.name}#${mergeComponentChild.fullName}`) @@ -616,7 +616,7 @@ describe('DecomposedMetadataTransformer', () => { it('should defer write operation for parent xml that is not a member of merge component', async () => { const { fullName, type } = component; const root = join('main', 'default', type.directoryName, fullName); - const componentToMerge = SourceComponent.createVirtualComponent( + const mergeWith = SourceComponent.createVirtualComponent( { name: 'a', type, @@ -633,7 +633,7 @@ describe('DecomposedMetadataTransformer', () => { const context = new ConvertContext(); const transformer = new DecomposedMetadataTransformer(registryAccess, context); - const result = await transformer.toSourceFormat(component, componentToMerge); + const result = await transformer.toSourceFormat({ component, mergeWith }); expect(result).to.be.empty; expect(context.decomposition.transactionState.get(`${type.name}#${fullName}`)).to.deep.equal({ origin: component, diff --git a/test/convert/transformers/defaultMetadataTransformer.test.ts b/test/convert/transformers/defaultMetadataTransformer.test.ts index 67e7572462..fc5e25e702 100644 --- a/test/convert/transformers/defaultMetadataTransformer.test.ts +++ b/test/convert/transformers/defaultMetadataTransformer.test.ts @@ -197,7 +197,7 @@ describe('DefaultMetadataTransformer', () => { source: component.tree.stream(component.xml), }); - expect(await transformer.toSourceFormat(component)).to.deep.equal(expectedInfos); + expect(await transformer.toSourceFormat({ component })).to.deep.equal(expectedInfos); }); it('should add in the -meta.xml suffix for components with no content', async () => { @@ -213,7 +213,7 @@ describe('DefaultMetadataTransformer', () => { }, ]; - expect(await transformer.toSourceFormat(component)).to.deep.equal(expectedInfos); + expect(await transformer.toSourceFormat({ component })).to.deep.equal(expectedInfos); }); it('should handle components in folders with no content', async () => { @@ -234,7 +234,7 @@ describe('DefaultMetadataTransformer', () => { }, ]; - expect(await transformer.toSourceFormat(component)).to.deep.equal(expectedInfos); + expect(await transformer.toSourceFormat({ component })).to.deep.equal(expectedInfos); }); it('should not remove file extension and preserve -meta.xml for DigitalExperienceBundle', async () => { @@ -263,7 +263,7 @@ describe('DefaultMetadataTransformer', () => { ), }, ]; - expect(await transformer.toSourceFormat(component)).to.deep.equal(expectedInfos); + expect(await transformer.toSourceFormat({ component })).to.deep.equal(expectedInfos); }); it('should handle folder components', async () => { @@ -284,7 +284,7 @@ describe('DefaultMetadataTransformer', () => { }, ]; - expect(await transformer.toSourceFormat(component)).to.deep.equal(expectedInfos); + expect(await transformer.toSourceFormat({ component })).to.deep.equal(expectedInfos); }); it('should merge output with merge component when content is a directory', async () => { @@ -322,7 +322,7 @@ describe('DefaultMetadataTransformer', () => { }, ]; - expect(await transformer.toSourceFormat(component, mergeWith)).to.deep.equal(expectedInfos); + expect(await transformer.toSourceFormat({ component, mergeWith })).to.deep.equal(expectedInfos); }); it('should merge output with merge component when content is a file', async () => { @@ -356,7 +356,7 @@ describe('DefaultMetadataTransformer', () => { }, ]; - expect(await transformer.toSourceFormat(component, mergeWith)).to.deep.equal(expectedInfos); + expect(await transformer.toSourceFormat({ component, mergeWith })).to.deep.equal(expectedInfos); }); it('should use merge component xml path', async () => { @@ -371,7 +371,7 @@ describe('DefaultMetadataTransformer', () => { [] ); assert(typeof component.xml === 'string'); - expect(await transformer.toSourceFormat(component, mergeWith)).to.deep.contain({ + expect(await transformer.toSourceFormat({ component, mergeWith })).to.deep.contain({ output: mergeWith.xml, source: component.tree.stream(component.xml), }); @@ -388,7 +388,7 @@ describe('DefaultMetadataTransformer', () => { ); assert(typeof component.xml === 'string'); - expect(await transformer.toSourceFormat(component, mergeWith)).to.deep.contain({ + expect(await transformer.toSourceFormat({ component, mergeWith })).to.deep.contain({ output: component.getPackageRelativePath(component.xml, 'source'), source: component.tree.stream(component.xml), }); @@ -411,7 +411,7 @@ describe('DefaultMetadataTransformer', () => { }, ]; - expect(await transformer.toSourceFormat(component)).to.deep.equal(expectedInfos); + expect(await transformer.toSourceFormat({ component })).to.deep.equal(expectedInfos); }); }); }); diff --git a/test/convert/transformers/nonDecomposedMetadataTransformer.test.ts b/test/convert/transformers/nonDecomposedMetadataTransformer.test.ts index 90b2219a29..faf7aa796b 100644 --- a/test/convert/transformers/nonDecomposedMetadataTransformer.test.ts +++ b/test/convert/transformers/nonDecomposedMetadataTransformer.test.ts @@ -40,7 +40,7 @@ describe('NonDecomposedMetadataTransformer', () => { const context = new ConvertContext(); const transformer = new NonDecomposedMetadataTransformer(registryAccess, context); - const result = await transformer.toSourceFormat(component); + const result = await transformer.toSourceFormat({ component }); expect(result).to.deep.equal([]); expect(context.decomposition.transactionState).to.deep.equal(new Map()); expect(context.recomposition.transactionState).to.deep.equal(new Map()); @@ -68,7 +68,7 @@ describe('NonDecomposedMetadataTransformer', () => { $$.SANDBOX.stub(componentToConvert, 'parseXml').resolves(nonDecomposed.FULL_XML_CONTENT); $$.SANDBOX.stub(componentToConvert, 'parseXmlSync').returns(nonDecomposed.FULL_XML_CONTENT); - const result = await transformer.toSourceFormat(componentToConvert, component); + const result = await transformer.toSourceFormat({ component: componentToConvert, mergeWith: component }); expect(result).to.deep.equal([]); expect(context.nonDecomposition.transactionState).to.deep.equal({ childrenByUniqueElement: new Map([ diff --git a/test/convert/transformers/staticResourceMetadataTransformer.test.ts b/test/convert/transformers/staticResourceMetadataTransformer.test.ts index f7f4043fb6..0e5b5d322e 100644 --- a/test/convert/transformers/staticResourceMetadataTransformer.test.ts +++ b/test/convert/transformers/staticResourceMetadataTransformer.test.ts @@ -222,7 +222,7 @@ describe('StaticResourceMetadataTransformer', () => { }, ]; - expect(await transformer.toSourceFormat(component)).to.deep.equalInAnyOrder(expectedInfos); + expect(await transformer.toSourceFormat({ component })).to.deep.equalInAnyOrder(expectedInfos); }); it('should rename extension from .resource for a fallback mime extension', async () => { @@ -247,7 +247,7 @@ describe('StaticResourceMetadataTransformer', () => { }, ]; - expect(await transformer.toSourceFormat(component)).to.deep.equalInAnyOrder(expectedInfos); + expect(await transformer.toSourceFormat({ component })).to.deep.equalInAnyOrder(expectedInfos); }); it('should rename extension from .resource for an unsupported mime extension', async () => { @@ -273,14 +273,14 @@ describe('StaticResourceMetadataTransformer', () => { }, ]; - expect(await transformer.toSourceFormat(component)).to.deep.equalInAnyOrder(expectedInfos); + expect(await transformer.toSourceFormat({ component })).to.deep.equalInAnyOrder(expectedInfos); }); it('should ignore components without content', async () => { const component = Object.assign({}, mixedContentSingleFile.COMPONENT); component.content = undefined; - expect(await transformer.toSourceFormat(component)).to.deep.equal([]); + expect(await transformer.toSourceFormat({ component })).to.deep.equal([]); }); it('should extract an archive', async () => { @@ -306,7 +306,7 @@ describe('StaticResourceMetadataTransformer', () => { }, ]; - expect(await transformer.toSourceFormat(component)).to.deep.equalInAnyOrder(expectedInfos); + expect(await transformer.toSourceFormat({ component })).to.deep.equalInAnyOrder(expectedInfos); expect(pipelineStub.callCount).to.equal(1); expect(pipelineStub.firstCall.args[1]).to.equal( join( @@ -340,7 +340,7 @@ describe('StaticResourceMetadataTransformer', () => { }, ]; - expect(await transformer.toSourceFormat(component)).to.deep.equalInAnyOrder(expectedInfos); + expect(await transformer.toSourceFormat({ component })).to.deep.equalInAnyOrder(expectedInfos); }); it('should merge output with merge component when content is archive', async () => { @@ -349,7 +349,7 @@ describe('StaticResourceMetadataTransformer', () => { assert(component.xml); assert(typeof transformer.defaultDirectory === 'string'); - const mergeComponent = SourceComponent.createVirtualComponent( + const mergeWith = SourceComponent.createVirtualComponent( { name: mixedContentSingleFile.COMPONENT.name, type: registry.types.staticresource, @@ -367,8 +367,8 @@ describe('StaticResourceMetadataTransformer', () => { }, ] ); - assert(mergeComponent.xml); - assert(mergeComponent.content); + assert(mergeWith.xml); + assert(mergeWith.content); env.stub(component, 'parseXml').resolves({ StaticResource: { contentType: 'application/zip', @@ -382,14 +382,14 @@ describe('StaticResourceMetadataTransformer', () => { const expectedInfos: WriteInfo[] = [ { source: component.tree.stream(component.xml), - output: mergeComponent.xml, + output: mergeWith.xml, }, ]; - expect(await transformer.toSourceFormat(component, mergeComponent)).to.deep.equal(expectedInfos); + expect(await transformer.toSourceFormat({ component, mergeWith })).to.deep.equal(expectedInfos); expect(pipelineStub.callCount).to.equal(1); expect(pipelineStub.firstCall.args[1]).to.deep.equal( - join(transformer.defaultDirectory, mergeComponent.content, 'b', 'c.css') + join(transformer.defaultDirectory, mergeWith.content, 'b', 'c.css') ); }); @@ -398,7 +398,7 @@ describe('StaticResourceMetadataTransformer', () => { const component = mixedContentSingleFile.COMPONENT; assert(component.content); assert(component.xml); - const mergeComponent = SourceComponent.createVirtualComponent( + const mergeWith = SourceComponent.createVirtualComponent( { name: mixedContentSingleFile.COMPONENT.name, type: registry.types.staticresource, @@ -416,7 +416,7 @@ describe('StaticResourceMetadataTransformer', () => { }, ] ); - assert(mergeComponent.xml); + assert(mergeWith.xml); env.stub(component, 'parseXml').resolves({ StaticResource: { @@ -426,15 +426,15 @@ describe('StaticResourceMetadataTransformer', () => { const expectedInfos: WriteInfo[] = [ { source: component.tree.stream(component.content), - output: `${mergeComponent.content}.txt`, + output: `${mergeWith.content}.txt`, }, { source: component.tree.stream(component.xml), - output: mergeComponent.xml, + output: mergeWith.xml, }, ]; - expect(await transformer.toSourceFormat(component, mergeComponent)).to.deep.equalInAnyOrder(expectedInfos); + expect(await transformer.toSourceFormat({ component, mergeWith })).to.deep.equalInAnyOrder(expectedInfos); }); }); }); diff --git a/test/mock/type-constants/decomposedCustomLabelsConstant.ts b/test/mock/type-constants/decomposedCustomLabelsConstant.ts new file mode 100644 index 0000000000..488f00f1bd --- /dev/null +++ b/test/mock/type-constants/decomposedCustomLabelsConstant.ts @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { join } from 'node:path'; + +import { SourceComponent, VirtualTreeContainer, presetMap, RegistryAccess } from '../../../src'; +import { getEffectiveRegistry } from '../../../src/registry/variants'; + +// Constants for a matching content file type +const regAcc = new RegistryAccess(getEffectiveRegistry({ presets: [presetMap.get('decomposeCustomLabelsBeta2')!] })); + +const customLabelsType = regAcc.getTypeByName('CustomLabels'); +const customLabelType = regAcc.getTypeByName('CustomLabel'); + +const MDAPI_XML_NAME = 'CustomLabels.labels'; + +export const EMPTY_CUSTOM_LABELS_CMP = new SourceComponent( + { + name: 'CustomLabels', + type: customLabelsType, + xml: join('labels', MDAPI_XML_NAME), + }, + new VirtualTreeContainer([ + { + dirPath: 'labels', + children: [ + { + name: MDAPI_XML_NAME, + data: Buffer.from(` +`), + }, + ], + }, + ]) +); +export const ONE_CUSTOM_LABELS_CMP = new SourceComponent( + { + name: 'CustomLabels', + type: customLabelsType, + xml: join('labels', MDAPI_XML_NAME), + }, + new VirtualTreeContainer([ + { + dirPath: 'labels', + children: [ + { + name: MDAPI_XML_NAME, + data: Buffer.from(` + + + OnlyLabel + en_US + true + OnlyLabel + OnlyLabel + +`), + }, + ], + }, + ]) +); + +export const THREE_CUSTOM_LABELS_CMP = new SourceComponent( + { + name: 'CustomLabels', + type: customLabelsType, + xml: join('labels', MDAPI_XML_NAME), + }, + new VirtualTreeContainer([ + { + dirPath: 'labels', + children: [ + { + name: MDAPI_XML_NAME, + data: Buffer.from(` + + + DeleteMe + en_US + true + DeleteMe + Test + + + KeepMe1 + en_US + true + KeepMe1 + Test + + + KeepMe2 + en_US + true + KeepMe2 + Test + +`), + }, + ], + }, + ]) +); + +const ONLY_LABEL_CONTENTS = ` + + OnlyLabel + en_US + true + OnlyLabel + OnlyLabel +`; + +export const ONLY_LABEL_CMP_IN_DEFAULT_DIR_CMP = new SourceComponent( + { + name: 'OnlyLabel', + type: customLabelType, + xml: join('main', 'default', 'labels', 'OnlyLabel.label-meta.xml'), + }, + new VirtualTreeContainer([ + { + dirPath: join('main', 'default', 'labels'), + children: [ + { + name: 'OnlyLabel.label-meta.xml', + data: Buffer.from(ONLY_LABEL_CONTENTS), + }, + ], + }, + ]) +); + +export const ONLY_LABEL_CMP_IN_ANOTHER_DIR_CMP = new SourceComponent( + { + name: 'OnlyLabel', + type: customLabelType, + xml: join('other', 'dir', 'labels', 'OnlyLabel.label-meta.xml'), + }, + new VirtualTreeContainer([ + { + dirPath: join('other', 'dir', 'labels'), + children: [ + { + name: 'OnlyLabel.label-meta.xml', + data: Buffer.from(ONLY_LABEL_CONTENTS), + }, + ], + }, + ]) +); + +export const ONLY_LABEL_NO_DIR_CMP = new SourceComponent( + { + name: 'OnlyLabel', + type: customLabelType, + xml: 'OnlyLabel.label-meta.xml', + }, + new VirtualTreeContainer([ + { + dirPath: '', + children: [ + { + name: 'OnlyLabel.label-meta.xml', + data: Buffer.from(ONLY_LABEL_CONTENTS), + }, + ], + }, + ]) +); + +export const OTHER_LABEL_CMP = new SourceComponent( + { + name: 'OtherLabel', + type: customLabelType, + xml: join('labels', 'OtherLabel.label-meta.xml'), + }, + new VirtualTreeContainer([ + { + dirPath: 'labels', + children: [ + { + name: 'OtherLabel.label-meta.xml', + data: Buffer.from(` + + OtherLabel + en_US + true + OtherLabel + OtherLabel +`), + }, + ], + }, + ]) +); diff --git a/test/registry/presetTesting.ts b/test/registry/presetTesting.ts index 2ac10e97c0..7c9ce16eb5 100644 --- a/test/registry/presetTesting.ts +++ b/test/registry/presetTesting.ts @@ -19,6 +19,8 @@ type RegistryIterator = { const registriesFromPresets = fs .readdirSync(presetFolder, { withFileTypes: true }) .filter((file) => file.name.endsWith('.json')) + // we don't want to test the original preset since it conflicts with the CustomLabelsBeta2 + .filter((file) => !file.name.endsWith('CustomLabelsBeta.json')) .map((file) => ({ name: file.name, registry: JSON.parse(fs.readFileSync(path.join(file.path, file.name), 'utf-8')) as MetadataRegistry, diff --git a/test/registry/registryValidation.test.ts b/test/registry/registryValidation.test.ts index 304fdf18b3..2ec410c5b5 100644 --- a/test/registry/registryValidation.test.ts +++ b/test/registry/registryValidation.test.ts @@ -5,7 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { assert, expect } from 'chai'; -import { DecompositionStrategy, MetadataType, TransformerStrategy } from '../../src/registry/types'; +import { MetadataType } from '../../src/registry/types'; import { metadataTypes as UnsupportedTypes } from '../../src/registry/nonSupportedTypes'; import { presets } from './presetTesting'; @@ -79,12 +79,14 @@ for (const preset of presets) { }); describe('every childTypes top-level property maps to a top-level type that has it in its childTypes', () => { - Object.entries(registry.childTypes).forEach(([childId, parentId]) => { - it(`childTypes member ${childId} matches a parent type ${parentId}`, () => { - expect(registry.types[parentId]).to.have.property('children'); - expect(registry.types[parentId]?.children?.types).to.have.property(childId); + Object.entries(registry.childTypes) + .filter(([, parentId]) => parentId) + .forEach(([childId, parentId]) => { + it(`childTypes member ${childId} matches a parent type ${parentId}`, () => { + expect(registry.types[parentId]).to.have.property('children'); + expect(registry.types[parentId]?.children?.types).to.have.property(childId); + }); }); - }); }); }); @@ -324,20 +326,13 @@ for (const preset of presets) { .forEach((type) => { it(`${type.id} has expected properties`, () => { assert(typeof type.strategies?.decomposition === 'string'); - expect( - [DecompositionStrategy.FolderPerType.valueOf(), DecompositionStrategy.TopLevel.valueOf()].includes( - type.strategies.decomposition - ) - ).to.be.true; + expect(['folderPerType', 'topLevel'].includes(type.strategies.decomposition)).to.be.true; assert(typeof type.strategies?.transformer === 'string'); expect( - [ - TransformerStrategy.Standard.valueOf(), - TransformerStrategy.Decomposed.valueOf(), - TransformerStrategy.StaticResource.valueOf(), - TransformerStrategy.NonDecomposed.valueOf(), - ].includes(type.strategies.transformer) + ['decomposed', 'nondecomposed', 'standard', 'staticResource', 'decomposedLabels'].includes( + type.strategies.transformer + ) ).to.be.true; expect(type.strategies.recomposition).to.be.undefined; }); diff --git a/test/snapshot/helper/conversions.ts b/test/snapshot/helper/conversions.ts index 81616fc7f7..e8781096af 100644 --- a/test/snapshot/helper/conversions.ts +++ b/test/snapshot/helper/conversions.ts @@ -9,8 +9,8 @@ import * as fs from 'node:fs'; import snap from 'mocha-snap'; import { expect, config, use } from 'chai'; import deepEqualInAnyOrder from 'deep-equal-in-any-order'; -import { XMLParser } from 'fast-xml-parser'; +import { parser } from '../../../src/utils/metadata'; import { RegistryAccess } from '../../../src/registry/registryAccess'; import { MetadataConverter } from '../../../src/convert/metadataConverter'; import { ComponentSetBuilder } from '../../../src/collections/componentSetBuilder'; @@ -92,18 +92,8 @@ export const sourceToMdapi = async (testDir: string): Promise => { }; /** checks that the two xml bodies have the same equivalent json (handles out-of-order things, etc) */ -export const compareTwoXml = (file1: string, file2: string): Chai.Assertion => { - const parser = new XMLParser({ - ignoreAttributes: false, - parseTagValue: false, - parseAttributeValue: false, - cdataPropName: '__cdata', - ignoreDeclaration: true, - numberParseOptions: { leadingZeros: false, hex: false }, - }); - - return expect(parser.parse(file1)).to.deep.equalInAnyOrder(parser.parse(file2)); -}; +export const compareTwoXml = (file1: string, file2: string): Chai.Assertion => + expect(parser.parse(file1)).to.deep.equalInAnyOrder(parser.parse(file2)); /** * catches missing files by asserting that two directories have the exact same children diff --git a/test/snapshot/sampleProjects/customLabels-simple/__snapshots__/verify-source-files.expected/force-app/test/snapshot/sampleProjects/customLabels-simple/force-app/main/default/labels/CustomLabels.labels-meta.xml b/test/snapshot/sampleProjects/customLabels-simple/__snapshots__/verify-source-files.expected/force-app/test/snapshot/sampleProjects/customLabels-simple/force-app/main/default/labels/CustomLabels.labels-meta.xml deleted file mode 100644 index d57a2391aa..0000000000 --- a/test/snapshot/sampleProjects/customLabels-simple/__snapshots__/verify-source-files.expected/force-app/test/snapshot/sampleProjects/customLabels-simple/force-app/main/default/labels/CustomLabels.labels-meta.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - DeleteMe - en_US - true - DeleteMe - Test - - - KeepMe1 - en_US - true - KeepMe1 - Test - - - KeepMe2 - en_US - true - KeepMe2 - Test - - diff --git a/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-md-files.expected/package.xml b/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-md-files.expected/package.xml index 51950dd57c..eec5a245c9 100644 --- a/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-md-files.expected/package.xml +++ b/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-md-files.expected/package.xml @@ -1,8 +1,10 @@ - CustomLabels - CustomLabels + DeleteMe + KeepMe1 + KeepMe2 + CustomLabel 60.0 diff --git a/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/CustomLabels.labels-meta.xml b/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/CustomLabels.labels-meta.xml deleted file mode 100644 index 6d9520ca90..0000000000 --- a/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/CustomLabels.labels-meta.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/DeleteMe.label-meta.xml b/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/DeleteMe.label-meta.xml similarity index 78% rename from test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/DeleteMe.label-meta.xml rename to test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/DeleteMe.label-meta.xml index 14bf9da669..95a5f3c818 100644 --- a/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/DeleteMe.label-meta.xml +++ b/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/DeleteMe.label-meta.xml @@ -1,5 +1,5 @@ - + DeleteMe en_US true diff --git a/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/KeepMe1.label-meta.xml b/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/KeepMe1.label-meta.xml similarity index 78% rename from test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/KeepMe1.label-meta.xml rename to test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/KeepMe1.label-meta.xml index 9a78141d6e..41a576cdc9 100644 --- a/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/KeepMe1.label-meta.xml +++ b/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/KeepMe1.label-meta.xml @@ -1,5 +1,5 @@ - + KeepMe1 en_US true diff --git a/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/KeepMe2.label-meta.xml b/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/KeepMe2.label-meta.xml similarity index 78% rename from test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/KeepMe2.label-meta.xml rename to test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/KeepMe2.label-meta.xml index 64ab9ef9b3..4c559666cb 100644 --- a/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/CustomLabels/KeepMe2.label-meta.xml +++ b/test/snapshot/sampleProjects/preset-decomposeLabels/__snapshots__/verify-source-files.expected/force-app/main/default/labels/KeepMe2.label-meta.xml @@ -1,5 +1,5 @@ - + KeepMe2 en_US true diff --git a/test/snapshot/sampleProjects/preset-decomposeLabels/sfdx-project.json b/test/snapshot/sampleProjects/preset-decomposeLabels/sfdx-project.json index 97d16c7028..ea1b12c4e1 100644 --- a/test/snapshot/sampleProjects/preset-decomposeLabels/sfdx-project.json +++ b/test/snapshot/sampleProjects/preset-decomposeLabels/sfdx-project.json @@ -7,7 +7,7 @@ "path": "force-app" } ], - "sourceBehaviorOptions": ["decomposeCustomLabelsBeta"], "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "60.0" + "sourceApiVersion": "60.0", + "sourceBehaviorOptions": ["decomposeCustomLabelsBeta2"] } diff --git a/yarn.lock b/yarn.lock index b285cfebd8..2a137e25b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -523,10 +523,10 @@ strip-ansi "6.0.1" ts-retry-promise "^0.8.1" -"@salesforce/core@^8.2.3", "@salesforce/core@^8.2.8", "@salesforce/core@^8.3.0": - version "8.3.0" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.3.0.tgz#b61fb6c0c0dec5664ce12ba62ebe35136ae33878" - integrity sha512-HZchC42oGJ5RQsG9HpAb1bT7ohjB31ATDz46ryMvLngMmrfHnyzv2mlIi6UdYkJ/2meH2BJkibHi8paPrtF+/A== +"@salesforce/core@^8.2.3", "@salesforce/core@^8.2.8", "@salesforce/core@^8.4.0": + version "8.4.0" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.4.0.tgz#d2ddfe07994c42b1e917e581e9cf47ad27b97a93" + integrity sha512-P+n0+Sp+v6voLTShW2E5sdF7gCUxEXJjNcc9Jtto0ZMyQesmQJ6WGpWmAuRoi+BVYc8OPSlEffndaYDAo/u73g== dependencies: "@jsforce/jsforce-node" "^3.4.0" "@salesforce/kit" "^3.1.6" @@ -584,7 +584,7 @@ typescript "^5.5.4" wireit "^0.14.5" -"@salesforce/kit@^3.1.6", "@salesforce/kit@^3.2.0": +"@salesforce/kit@^3.1.6", "@salesforce/kit@^3.2.0", "@salesforce/kit@^3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@salesforce/kit/-/kit-3.2.1.tgz#3de2c9ff52710a169fc887716d97c00d26065c56" integrity sha512-LrZH2F06XPLUTMXC3av6A0VDAJykUqRtYB6tTjAKzwS1gCnp6BEn6VyjZNg0Fg/Kfp6OTrMxiIgbUFsNehEV7A== @@ -5071,16 +5071,7 @@ srcset@^5.0.0: resolved "https://registry.yarnpkg.com/srcset/-/srcset-5.0.0.tgz#9df6c3961b5b44a02532ce6ae4544832609e2e3f" integrity sha512-SqEZaAEhe0A6ETEa9O1IhSPC7MdvehZtCnTR0AftXk3QhY2UNgb+NApFOUPZILXk/YTDfFxMTNJOBpzrJsEdIA== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5139,14 +5130,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5636,7 +5620,7 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -5654,15 +5638,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From f26028380d1d3e3647e576fe2cedf430a3dbfe7f Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Thu, 15 Aug 2024 13:29:53 -0600 Subject: [PATCH 2/3] feat: bump to 12.3.0 (#1399) bumping to release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac06f110c2..2c608f346b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/source-deploy-retrieve", - "version": "12.3.0-qa.1", + "version": "12.3.0", "description": "JavaScript library to run Salesforce metadata deploys and retrieves", "main": "lib/src/index.js", "author": "Salesforce", From 7d0821bb6545d10638cc22252460745b3eb815db Mon Sep 17 00:00:00 2001 From: svc-cli-bot Date: Thu, 15 Aug 2024 19:30:21 +0000 Subject: [PATCH 3/3] chore(release): 12.4.0 [skip ci] --- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4f2ac067d..9f30abad85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# [12.4.0](https://github.com/forcedotcom/source-deploy-retrieve/compare/12.2.1...12.4.0) (2024-08-15) + + +### Features + +* bump to 12.3.0 ([#1399](https://github.com/forcedotcom/source-deploy-retrieve/issues/1399)) ([f260283](https://github.com/forcedotcom/source-deploy-retrieve/commit/f26028380d1d3e3647e576fe2cedf430a3dbfe7f)) +* custom label beta 2 ([#1392](https://github.com/forcedotcom/source-deploy-retrieve/issues/1392)) ([51cbe84](https://github.com/forcedotcom/source-deploy-retrieve/commit/51cbe848959f580c1b9a2e6816e8f33e89a2bd64)) + + + ## [12.2.1](https://github.com/forcedotcom/source-deploy-retrieve/compare/12.2.0...12.2.1) (2024-08-12) diff --git a/package.json b/package.json index 2c608f346b..cefa534ed3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/source-deploy-retrieve", - "version": "12.3.0", + "version": "12.4.0", "description": "JavaScript library to run Salesforce metadata deploys and retrieves", "main": "lib/src/index.js", "author": "Salesforce",