From 1c4e1ae6ad65bce42963d01a0db08b9e0b984066 Mon Sep 17 00:00:00 2001 From: AnanyaJha Date: Fri, 20 Aug 2021 13:02:11 -0700 Subject: [PATCH] Port pr v4.1.1 (#432) * fix: export all public TS types and interfaces (#423) * fix: making exports in the right format @W-9727845@ * chore: removing yarn changes * fix: xml nodes might not be arrays (#424) Co-authored-by: Willie Ruemmele * fix: throw an error for unexpected child types (#426) * fix: throw an error for unexpected child types * fix: throw better error when transforming invalid child * fix: ensure valid children during retrieve Co-authored-by: Willie Ruemmele * fix: force fullName to CustomLabels (#427) * fix: force fullName to CustomLabels * chore: update prop name * fix: backward support for toolbelt suffix differences * Revert "fix: backward support for toolbelt suffix differences" This reverts commit 9cf9505aa474132de958154a7122b46480518745. Co-authored-by: mshanemc Co-authored-by: Willie Ruemmele * fix: forceIgnore does not work for `SFDX: Retrieve Source in Manifest from Org` in vscode extension (#413) * fix: forceIgnore does not work with retrieve manifest * fix: matchingContent subfolder * Wr/content type of undefined (#429) * chore: initial pass on preventing improperly structured static resources * chore: added error message Co-authored-by: Shane McLaughlin * fix: support toolbelt suffixes (#428) * fix: force fullName to CustomLabels * chore: update prop name * fix: backward support for toolbelt suffix differences * Revert "fix: backward support for toolbelt suffix differences" This reverts commit 9cf9505aa474132de958154a7122b46480518745. * fix: support toolbelt suffixes * feat: support legacy suffix conversion Co-authored-by: Mike Donnalley Co-authored-by: Willie Ruemmele Co-authored-by: vamsimundra <85279195+vamsimundra@users.noreply.github.com> Co-authored-by: Shane McLaughlin Co-authored-by: Willie Ruemmele Co-authored-by: Steve Hetzel Co-authored-by: Mike Donnalley Co-authored-by: Violet Yao --- src/client/index.ts | 40 ++++++-- src/client/metadataApiRetrieve.ts | 5 +- src/client/metadataTransfer.ts | 4 + src/collections/componentSet.ts | 4 + src/collections/index.ts | 8 +- src/common/index.ts | 2 +- src/convert/convertContext.ts | 7 +- src/convert/index.ts | 9 +- src/convert/metadataConverter.ts | 1 + src/convert/streams.ts | 20 +++- .../decomposedMetadataTransformer.ts | 38 +++++--- .../defaultMetadataTransformer.ts | 6 +- .../staticResourceMetadataTransformer.ts | 10 +- src/convert/types.ts | 1 + src/i18n/i18n.ts | 3 + src/index.ts | 87 ++++++++++++++--- src/registry/index.ts | 3 +- src/registry/registry.json | 8 +- src/registry/types.ts | 10 ++ .../adapters/decomposedSourceAdapter.ts | 7 +- src/resolve/index.ts | 9 +- src/resolve/metadataResolver.ts | 3 + src/resolve/sourceComponent.ts | 3 + test/client/metadataApiRetrieve.test.ts | 16 +++ .../decomposedMetadataTransformer.ts | 97 ++++++++++++++++++- .../staticResourceMetadataTransformer.ts | 20 ++++ test/mock/client/transferOperations.ts | 9 +- .../adapters/decomposedSourceAdapter.ts | 42 +++++++- 28 files changed, 413 insertions(+), 59 deletions(-) diff --git a/src/client/index.ts b/src/client/index.ts index 89af6497cc..864594d5b6 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -13,15 +13,43 @@ export { } from './metadataApiRetrieve'; export { ToolingApi, ToolingDeployOptions, ToolingRetrieveOptions } from './toolingApi'; export { - AsyncResult, + ComponentDeployment, + ComponentRetrieval, + ComponentDiagnostic, FileResponse, + SourceApiResult, + AsyncResult, + SourceDeployResult, + RequestStatus, + MetadataRequestStatus, + RetrieveFailure, + RetrieveSuccess, + SourceRetrieveResult, MetadataApiDeployStatus, + DeployDetails, + RunTestResult, + CodeCoverage, + LocationsNotCovered, + CodeCoverageWarnings, + Failures, + Successes, + DeployMessage, + RetrieveRequest, + RetrieveMessage, + FileProperties, + ComponentStatus, MetadataApiRetrieveStatus, + PackageOption, + PackageOptions, RetrieveOptions, - SourceDeployResult, - RetrieveMessage, - SourceRetrieveResult, + ContainerAsyncRequest, ToolingDeployStatus, - ComponentStatus, - FileProperties, + QueryResult, + ApexRecord, + VFRecord, + AuraRecord, + LWCRecord, + ToolingCreateResult, + AuraDefinition, + LightningComponentResource, } from './types'; diff --git a/src/client/metadataApiRetrieve.ts b/src/client/metadataApiRetrieve.ts index 8e1b86cf14..64d5b34d8e 100644 --- a/src/client/metadataApiRetrieve.ts +++ b/src/client/metadataApiRetrieve.ts @@ -216,6 +216,7 @@ export class MetadataApiRetrieve extends MetadataTransfer< type: 'merge', mergeWith: this.components.getSourceComponents(), defaultDirectory: pkg.outputDir, + forceIgnoredPaths: this.components.forceIgnoredPaths ?? new Set(), } : { type: 'directory', @@ -229,7 +230,9 @@ export class MetadataApiRetrieve extends MetadataTransfer< .getSourceComponents() .toArray(); const convertResult = await converter.convert(zipComponents, 'source', outputConfig); - components.push(...convertResult.converted); + if (convertResult) { + components.push(...convertResult.converted); + } } return new ComponentSet(components, registry); } diff --git a/src/client/metadataTransfer.ts b/src/client/metadataTransfer.ts index 07b10c2df1..43ca29722f 100644 --- a/src/client/metadataTransfer.ts +++ b/src/client/metadataTransfer.ts @@ -111,6 +111,10 @@ export abstract class MetadataTransfer< return result; } catch (e) { const error = new MetadataTransferError('md_request_fail', e.message); + if (error.stack && e.stack) { + // append the original stack to this new error + error.stack += `\nDUE TO:\n${e.stack}`; + } if (this.event.listenerCount('error') === 0) { throw error; } diff --git a/src/collections/componentSet.ts b/src/collections/componentSet.ts index a544d0e417..12e59565b1 100644 --- a/src/collections/componentSet.ts +++ b/src/collections/componentSet.ts @@ -58,6 +58,7 @@ export class ComponentSet extends LazyCollection { */ public sourceApiVersion: string; public fullName?: string; + public forceIgnoredPaths?: Set; private logger: Logger; private registry: RegistryAccess; private components = new Map>(); @@ -137,6 +138,8 @@ export class ComponentSet extends LazyCollection { buildComponents(fsPaths, false); buildComponents(fsDeletePaths, true); + set.forceIgnoredPaths = resolver.forceIgnoredPaths; + return set; } @@ -192,6 +195,7 @@ export class ComponentSet extends LazyCollection { include: resolveIncludeSet, registry: options.registry, }); + result.forceIgnoredPaths = components.forceIgnoredPaths; for (const component of components) { result.add(component); } diff --git a/src/collections/index.ts b/src/collections/index.ts index 2d7f403218..edaf009018 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -6,4 +6,10 @@ */ export { LazyCollection } from './lazyCollection'; export { ComponentSet, DeploySetOptions, RetrieveSetOptions } from './componentSet'; -export { FromSourceOptions, FromManifestOptions } from './types'; +export { + PackageTypeMembers, + PackageManifestObject, + DestructiveChangesType, + FromSourceOptions, + FromManifestOptions, +} from './types'; diff --git a/src/common/index.ts b/src/common/index.ts index 7ad08d7d5f..18536f71d9 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -4,7 +4,7 @@ * 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 */ -export { SourcePath, OptionalTreeRegistryOptions } from './types'; +export { SourcePath, TreeOptions, OptionalTreeRegistryOptions, RegistryOptions } from './types'; export { DEFAULT_PACKAGE_ROOT_SFDX, META_XML_SUFFIX, diff --git a/src/convert/convertContext.ts b/src/convert/convertContext.ts index 266a7d7a57..b06780c877 100644 --- a/src/convert/convertContext.ts +++ b/src/convert/convertContext.ts @@ -11,6 +11,7 @@ import { JsToXml } from './streams'; import { META_XML_SUFFIX, XML_NS_KEY, XML_NS_URL } from '../common'; import { getString, JsonArray, JsonMap } from '@salesforce/ts-types'; import { ComponentSet } from '../collections'; +import { normalizeToArray } from '../utils/collections'; import { RecompositionStrategy } from '../registry/types'; import { isEmpty } from '@salesforce/kit'; @@ -89,7 +90,8 @@ class RecompositionFinalizer extends ConvertTransactionFinalizer = new Set(); protected rootDestination?: SourcePath; constructor(rootDestination?: SourcePath) { @@ -158,6 +159,19 @@ export class StandardWriter extends ComponentWriter { const fullDest = isAbsolute(info.output) ? info.output : join(this.rootDestination, info.output); + if (!fs.fileExistsSync(fullDest)) { + for (const ignoredPath of this.forceIgnoredPaths) { + if ( + dirname(ignoredPath).includes(dirname(fullDest)) && + basename(ignoredPath).includes(basename(fullDest)) + ) { + return; + } + } + } + if (this.forceIgnoredPaths.has(fullDest)) { + return; + } // if there are children, resolve each file. o/w just pick one of the files to resolve if (toResolve.length === 0 || chunk.component.type.children) { // This is a workaround for a server side ListViews bug where diff --git a/src/convert/transformers/decomposedMetadataTransformer.ts b/src/convert/transformers/decomposedMetadataTransformer.ts index 4e3aa4c14b..ee1d4e3931 100644 --- a/src/convert/transformers/decomposedMetadataTransformer.ts +++ b/src/convert/transformers/decomposedMetadataTransformer.ts @@ -14,6 +14,7 @@ import { SourcePath, META_XML_SUFFIX, XML_NS_URL, XML_NS_KEY } from '../../commo import { ComponentSet } from '../../collections'; import { DecompositionState } from '../convertContext'; import { DecompositionStrategy } from '../../registry'; +import { TypeInferenceError } from '../../errors'; export class DecomposedMetadataTransformer extends BaseMetadataTransformer { public async toMetadataFormat(component: SourceComponent): Promise { @@ -32,16 +33,10 @@ export class DecomposedMetadataTransformer extends BaseMetadataTransformer { } else { const { fullName } = component; this.context.recomposition.setState((state) => { - if (state[fullName]) { - for (const child of component.getChildren()) { - state[fullName].children.add(child); - } - } else { - state[fullName] = { - component, - children: new ComponentSet(component.getChildren(), this.registry), - }; + if (!state[fullName]) { + state[fullName] = { component, children: new ComponentSet([], this.registry) }; } + state[fullName].children = this.ensureValidChildren(component, state[fullName].children); }); } // noop since the finalizer will push the writes to the component writer @@ -53,11 +48,9 @@ export class DecomposedMetadataTransformer extends BaseMetadataTransformer { mergeWith?: SourceComponent ): Promise { const writeInfos: WriteInfo[] = []; + const childrenOfMergeComponent = mergeWith && this.ensureValidChildren(mergeWith); const { type, fullName: parentFullName } = component; - const childrenOfMergeComponent = mergeWith - ? new ComponentSet(mergeWith.getChildren(), this.registry) - : undefined; let parentXmlObject: JsonMap; const composedMetadata = await this.getComposedMetadataEntries(component); @@ -147,6 +140,27 @@ export class DecomposedMetadataTransformer extends BaseMetadataTransformer { return writeInfos; } + // Ensures that the children of the provided SourceComponent are valid child + // types before adding them to the returned ComponentSet. Invalid child types + // can occur when projects are structured in an atypical way such as having + // ApexClasses or Layouts within a CustomObject folder. + private ensureValidChildren(component: SourceComponent, compSet?: ComponentSet): ComponentSet { + compSet = compSet || new ComponentSet([], this.registry); + const validChildTypes = Object.keys(component.type.children.types); + for (const child of component.getChildren()) { + // Ensure only valid child types are included with the parent. + if (!validChildTypes.includes(child.type?.id)) { + const filePath = child.xml || child.content; + throw new TypeInferenceError('error_unexpected_child_type', [ + filePath, + component.type.name, + ]); + } + compSet.add(child); + } + return compSet; + } + private async getComposedMetadataEntries(component: SourceComponent): Promise<[string, any][]> { const composedMetadata = (await component.parseXml())[component.type.name]; return Object.entries(composedMetadata); diff --git a/src/convert/transformers/defaultMetadataTransformer.ts b/src/convert/transformers/defaultMetadataTransformer.ts index 5a2b72a51e..ab6b603e0c 100644 --- a/src/convert/transformers/defaultMetadataTransformer.ts +++ b/src/convert/transformers/defaultMetadataTransformer.ts @@ -86,7 +86,7 @@ export class DefaultMetadataTransformer extends BaseMetadataTransformer { return mergeWith.xml; } - const { folderContentType, suffix } = component.type; + const { folderContentType, suffix, legacySuffix } = component.type; let xmlDestination = component.getPackageRelativePath(component.xml, targetFormat); // quirks: @@ -117,7 +117,9 @@ export class DefaultMetadataTransformer extends BaseMetadataTransformer { ); } } - + if (legacySuffix && suffix && xmlDestination.includes(legacySuffix)) { + xmlDestination = xmlDestination.replace(legacySuffix, suffix); + } return xmlDestination; } } diff --git a/src/convert/transformers/staticResourceMetadataTransformer.ts b/src/convert/transformers/staticResourceMetadataTransformer.ts index f62d04f817..7b9e0b1dca 100644 --- a/src/convert/transformers/staticResourceMetadataTransformer.ts +++ b/src/convert/transformers/staticResourceMetadataTransformer.ts @@ -141,7 +141,15 @@ export class StaticResourceMetadataTransformer extends BaseMetadataTransformer { } private async getContentType(component: SourceComponent): Promise { - return ((await component.parseXml()).StaticResource as JsonMap).contentType as string; + try { + return ((await component.parseXml()).StaticResource as JsonMap).contentType as string; + } catch (e) { + if ((e as Error).message.includes("Cannot read property 'contentType' of undefined")) { + throw new LibraryError('error_static_resource_missing_resource_file', [ + join('staticresources', component.name), + ]); + } + } } private getExtensionFromType(contentType: string): string { diff --git a/src/convert/types.ts b/src/convert/types.ts index f1c1139469..5df5390171 100644 --- a/src/convert/types.ts +++ b/src/convert/types.ts @@ -64,6 +64,7 @@ export type MergeConfig = { * Location to store components that aren't merged. */ defaultDirectory: SourcePath; + forceIgnoredPaths?: Set; }; /** diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index f0f91117c0..fb9c677f43 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -30,6 +30,7 @@ export const messages = { error_convert_invalid_format: "Invalid conversion format '%s'", error_convert_not_implemented: '%s format conversion not yet implemented for type %s', error_could_not_infer_type: '%s: Could not infer a metadata type', + error_unexpected_child_type: 'Unexpected child metadata [%s] found for parent type [%s]', error_expected_source_files: "%s: Expected source files for type '%s'", error_failed_convert: 'Component conversion failed: %s', error_merge_metadata_target_unsupported: @@ -60,6 +61,8 @@ export const messages = { error_invalid_package: 'The metadata pacakge was not initialized properly', error_static_resource_expected_archive_type: 'A StaticResource directory must have a content type of application/zip or application/jar - found %s for %s', + error_static_resource_missing_resource_file: + 'A StaticResource must have an associated .resource file, missing %s.resource-meta.xml', error_no_job_id: 'The %s operation is missing a job ID. Initialize an operation with an ID, or start a new job.', tapi_deploy_component_limit_error: diff --git a/src/index.ts b/src/index.ts index 9fc00c0533..8bdfd87a76 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,45 +5,102 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ export { - AsyncResult, MetadataApiDeploy, MetadataApiDeployOptions, + DeployResult, MetadataApiRetrieve, MetadataApiRetrieveOptions, RetrieveResult, ToolingApi, ToolingDeployOptions, ToolingRetrieveOptions, - DeployResult, + ComponentDeployment, + ComponentRetrieval, + ComponentDiagnostic, FileResponse, + SourceApiResult, + AsyncResult, + SourceDeployResult, + RequestStatus, + MetadataRequestStatus, + RetrieveFailure, + RetrieveSuccess, + SourceRetrieveResult, MetadataApiDeployStatus, + DeployDetails, + RunTestResult, + CodeCoverage, + LocationsNotCovered, + CodeCoverageWarnings, + Failures, + Successes, + DeployMessage, + RetrieveRequest, + RetrieveMessage, + FileProperties, + ComponentStatus, MetadataApiRetrieveStatus, + PackageOption, + PackageOptions, RetrieveOptions, - SourceDeployResult, - RetrieveMessage, - SourceRetrieveResult, + ContainerAsyncRequest, ToolingDeployStatus, - ComponentStatus, - FileProperties, + QueryResult, + ApexRecord, + VFRecord, + AuraRecord, + LWCRecord, + ToolingCreateResult, + AuraDefinition, + LightningComponentResource, } from './client'; -export { MetadataConverter, ConvertOutputConfig, ConvertResult } from './convert'; export { - MetadataComponent, - MetadataMember, + MetadataConverter, + WriteInfo, + WriterFormat, + DirectoryConfig, + ZipConfig, + MergeConfig, + MetadataTransformer, + SfdxFileFormat, + ConvertOutputConfig, + ConvertResult, +} from './convert'; +export { MetadataResolver, + ManifestResolver, + TreeContainer, + NodeFSTreeContainer, VirtualTreeContainer, ZipTreeContainer, SourceComponent, - TreeContainer, + MetadataComponent, + MetadataMember, + ComponentLike, + MetadataXml, + VirtualFile, VirtualDirectory, + SourceAdapter, ForceIgnore, } from './resolve'; -export { SourcePath } from './common'; +export { SourcePath, TreeOptions, OptionalTreeRegistryOptions, RegistryOptions } from './common'; export { + LazyCollection, ComponentSet, - FromManifestOptions, - FromSourceOptions, DeploySetOptions, RetrieveSetOptions, + PackageTypeMembers, + PackageManifestObject, + DestructiveChangesType, + FromSourceOptions, + FromManifestOptions, } from './collections'; -export { MetadataType, RegistryAccess, registry } from './registry'; +export { + RegistryAccess, + registry, + MetadataRegistry, + MetadataType, + DecompositionStrategy, + RecompositionStrategy, + TransformerStrategy, +} from './registry'; diff --git a/src/registry/index.ts b/src/registry/index.ts index bc49c83762..e9ce506849 100644 --- a/src/registry/index.ts +++ b/src/registry/index.ts @@ -7,8 +7,9 @@ export { registry } from './registry'; export { RegistryAccess } from './registryAccess'; export { - MetadataType, MetadataRegistry, + MetadataType, DecompositionStrategy, + RecompositionStrategy, TransformerStrategy, } from './types'; diff --git a/src/registry/registry.json b/src/registry/registry.json index 92e2e18679..4e89c426fe 100644 --- a/src/registry/registry.json +++ b/src/registry/registry.json @@ -11,6 +11,7 @@ "customlabels": { "id": "customlabels", "name": "CustomLabels", + "ignoreParsedFullName": true, "suffix": "labels", "directoryName": "labels", "inFolder": false, @@ -319,6 +320,7 @@ "suffixes": { "field": "customfield", "index": "index", + "indexe": "index", "businessProcess": "businessprocess", "recordType": "recordtype", "compactLayout": "compactlayout", @@ -421,7 +423,8 @@ "suffix": "weblink", "directoryName": "weblinks", "inFolder": false, - "strictDirectoryName": false + "strictDirectoryName": false, + "legacySuffix": "custompageweblink" }, "letterhead": { "id": "letterhead", @@ -2384,6 +2387,7 @@ "feedFilter": "customfeedfilter", "layout": "layout", "weblink": "custompageweblink", + "custompageweblink": "custompageweblink", "letter": "letterhead", "email": "emailtemplate", "quickAction": "quickaction", @@ -2563,7 +2567,7 @@ "bots": "bot", "objectTranslations": "customobjecttranslation", "staticresources": "staticresource", - "sites": "customsite", + "sites": "customsite" }, "childTypes": { "customlabel": "customlabels", diff --git a/src/registry/types.ts b/src/registry/types.ts index 5b98569b6b..c8938774c3 100644 --- a/src/registry/types.ts +++ b/src/registry/types.ts @@ -57,6 +57,11 @@ export interface MetadataType { * Whether or not components are required to reside in a folder named after the type's directoryName. */ strictDirectoryName?: boolean; + /** + * Whether or not to ignore the fullName that's parsed from the file path. If true, the metadata type's + * name will be used instead. For example, CustomLabels instead of MyLabels. + */ + ignoreParsedFullName?: boolean; /** * If the type is a folder type (container for components), the id of the type it is a container for. */ @@ -69,6 +74,11 @@ export interface MetadataType { * If the parent name should be ignored when constructing the type's fullName */ ignoreParentName?: boolean; + /** + * When converting deploying source, this will update the suffix in the output or temporary directory (metadata format) + * Use this, along with additional suffix keys in the registry, to support incorrect suffixes from existing code + */ + legacySuffix?: string; /** * The xml attribute used as the unique identifier when parsing the xml */ diff --git a/src/resolve/adapters/decomposedSourceAdapter.ts b/src/resolve/adapters/decomposedSourceAdapter.ts index b1fa58ae0e..e724a80283 100644 --- a/src/resolve/adapters/decomposedSourceAdapter.ts +++ b/src/resolve/adapters/decomposedSourceAdapter.ts @@ -9,7 +9,7 @@ import { SourcePath } from '../../common'; import { SourceComponent } from '../sourceComponent'; import { baseName, parentName, parseMetadataXml } from '../../utils'; import { DecompositionStrategy } from '../../registry'; -import { UnexpectedForceIgnore } from '../../errors'; +import { TypeInferenceError } from '../../errors'; /** * Handles decomposed types. A flavor of mixed content where a component can @@ -115,6 +115,11 @@ export class DecomposedSourceAdapter extends MixedContentSourceAdapter { ); } if (!triggerIsAChild) { + if (!component) { + // This is most likely metadata found within a CustomObject folder that is not a + // child type of CustomObject. E.g., Layout, SharingRules, ApexClass. + throw new TypeInferenceError('error_unexpected_child_type', [trigger, this.type.name]); + } component.content = pathToContent; } } diff --git a/src/resolve/index.ts b/src/resolve/index.ts index 8ef76d25de..224008a384 100644 --- a/src/resolve/index.ts +++ b/src/resolve/index.ts @@ -14,11 +14,12 @@ export { } from './treeContainers'; export { SourceComponent } from './sourceComponent'; export { - MetadataXml, - SourceAdapter, - VirtualDirectory, MetadataComponent, - ComponentLike, MetadataMember, + ComponentLike, + MetadataXml, + VirtualFile, + VirtualDirectory, + SourceAdapter, } from './types'; export { ForceIgnore } from './forceIgnore'; diff --git a/src/resolve/metadataResolver.ts b/src/resolve/metadataResolver.ts index e64adc8984..eba9d7fd91 100644 --- a/src/resolve/metadataResolver.ts +++ b/src/resolve/metadataResolver.ts @@ -20,6 +20,7 @@ import { MetadataType } from '../registry'; * @internal */ export class MetadataResolver { + public forceIgnoredPaths: Set; private forceIgnore: ForceIgnore; private sourceAdapterFactory: SourceAdapterFactory; private tree: TreeContainer; @@ -33,6 +34,7 @@ export class MetadataResolver { this.registry = registry; this.tree = tree; this.sourceAdapterFactory = new SourceAdapterFactory(this.registry, tree); + this.forceIgnoredPaths = new Set(); } /** @@ -118,6 +120,7 @@ export class MetadataResolver { private resolveComponent(fsPath: string, isResolvingSource: boolean): SourceComponent { if (this.forceIgnore.denies(fsPath)) { // don't resolve the component if the path is denied + this.forceIgnoredPaths.add(fsPath); return; } const type = this.resolveType(fsPath); diff --git a/src/resolve/sourceComponent.ts b/src/resolve/sourceComponent.ts index 30e67dcb0b..128f1458de 100644 --- a/src/resolve/sourceComponent.ts +++ b/src/resolve/sourceComponent.ts @@ -218,6 +218,9 @@ export class SourceComponent implements MetadataComponent { } get fullName(): string { + if (this.type.ignoreParsedFullName) { + return this.type.name; + } if (this.parent && this.type.ignoreParentName) { return this.name; } else { diff --git a/test/client/metadataApiRetrieve.test.ts b/test/client/metadataApiRetrieve.test.ts index 9afe2293f9..1557f68aa2 100644 --- a/test/client/metadataApiRetrieve.test.ts +++ b/test/client/metadataApiRetrieve.test.ts @@ -336,6 +336,7 @@ describe('MetadataApiRetrieve', async () => { type: 'merge', mergeWith: toRetrieve.getSourceComponents(), defaultDirectory: MOCK_DEFAULT_OUTPUT, + forceIgnoredPaths: new Set(), }) ).to.be.true; }); @@ -355,6 +356,21 @@ describe('MetadataApiRetrieve', async () => { expect(result).to.deep.equal(expected); }); + it('should construct a result object with no components when components are forceIgnored', async () => { + const toRetrieve = new ComponentSet([COMPONENT], mockRegistry); + toRetrieve.forceIgnoredPaths = new Set([COMPONENT.xml, COMPONENT.content]); + const { operation } = await stubMetadataRetrieve(env, { + toRetrieve, + merge: true, + successes: toRetrieve, + }); + + await operation.start(); + const result = await operation.pollStatus(); + + expect(result.components.size).to.equal(0); + }); + it('should construct a result object with no components when no components are retrieved', async () => { const toRetrieve = new ComponentSet([COMPONENT], mockRegistry); const { operation, response } = await stubMetadataRetrieve(env, { diff --git a/test/convert/transformers/decomposedMetadataTransformer.ts b/test/convert/transformers/decomposedMetadataTransformer.ts index df745d5f97..1ae71244ea 100644 --- a/test/convert/transformers/decomposedMetadataTransformer.ts +++ b/test/convert/transformers/decomposedMetadataTransformer.ts @@ -5,16 +5,25 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { mockRegistry, mockRegistryData, decomposed } from '../../mock/registry'; +import { + mockRegistry, + mockRegistryData, + decomposed, + matchingContentFile, +} from '../../mock/registry'; import { DecomposedMetadataTransformer } from '../../../src/convert/transformers/decomposedMetadataTransformer'; import { expect } from 'chai'; import { createSandbox } from 'sinon'; import { join } from 'path'; +import { baseName } from '../../../src/utils'; import { JsToXml } from '../../../src/convert/streams'; import { DECOMPOSED_TOP_LEVEL_COMPONENT } from '../../mock/registry/type-constants/decomposedTopLevelConstants'; import { ComponentSet, SourceComponent } from '../../../src'; import { XML_NS_URL, XML_NS_KEY } from '../../../src/common'; import { ConvertContext } from '../../../src/convert/convertContext'; +import { TypeInferenceError } from '../../../src/errors'; +import { nls } from '../../../src/i18n'; +import { assert } from '@salesforce/ts-types'; const env = createSandbox(); @@ -66,6 +75,48 @@ describe('DecomposedMetadataTransformer', () => { }, }); }); + + it('should throw when an invalid child is included with the parent', async () => { + const { CONTENT_NAMES, XML_NAMES } = matchingContentFile; + const fsUnexpectedChild = [ + { + dirPath: decomposed.DECOMPOSED_PATH, + children: [ + decomposed.DECOMPOSED_CHILD_XML_NAME_1, + decomposed.DECOMPOSED_CHILD_DIR, + 'classes', + ], + }, + { + dirPath: decomposed.DECOMPOSED_CHILD_DIR_PATH, + children: [decomposed.DECOMPOSED_CHILD_XML_NAME_2], + }, + { + dirPath: join(decomposed.DECOMPOSED_PATH, 'classes'), + children: [CONTENT_NAMES[0], XML_NAMES[0]], + }, + ]; + const parentComponent = SourceComponent.createVirtualComponent( + { + name: baseName(decomposed.DECOMPOSED_XML_PATH), + type: decomposed.DECOMPOSED_COMPONENT.type, + xml: decomposed.DECOMPOSED_XML_PATH, + content: decomposed.DECOMPOSED_PATH, + }, + fsUnexpectedChild + ); + const fsPath = join(decomposed.DECOMPOSED_PATH, 'classes', XML_NAMES[0]); + const transformer = new DecomposedMetadataTransformer(mockRegistry, new ConvertContext()); + + try { + await transformer.toMetadataFormat(parentComponent); + assert(false, 'expected TypeInferenceError to be thrown'); + } catch (err) { + expect(err).to.be.instanceOf(TypeInferenceError); + const msg = nls.localize('error_unexpected_child_type', [fsPath, component.type.name]); + expect(err.message).to.equal(msg); + } + }); }); describe('toSourceFormat', () => { @@ -306,6 +357,50 @@ describe('DecomposedMetadataTransformer', () => { ]); }); + it('should throw when an invalid merge child is included with the parent', async () => { + const { CONTENT_NAMES, XML_NAMES } = matchingContentFile; + const fsUnexpectedChild = [ + { + dirPath: decomposed.DECOMPOSED_PATH, + children: [ + decomposed.DECOMPOSED_CHILD_XML_NAME_1, + decomposed.DECOMPOSED_CHILD_DIR, + 'classes', + ], + }, + { + dirPath: decomposed.DECOMPOSED_CHILD_DIR_PATH, + children: [decomposed.DECOMPOSED_CHILD_XML_NAME_2], + }, + { + dirPath: join(decomposed.DECOMPOSED_PATH, 'classes'), + children: [CONTENT_NAMES[0], XML_NAMES[0]], + }, + ]; + const parentComponent = SourceComponent.createVirtualComponent( + { + name: baseName(decomposed.DECOMPOSED_XML_PATH), + type: decomposed.DECOMPOSED_COMPONENT.type, + xml: decomposed.DECOMPOSED_XML_PATH, + content: decomposed.DECOMPOSED_PATH, + }, + fsUnexpectedChild + ); + const fsPath = join(decomposed.DECOMPOSED_PATH, 'classes', XML_NAMES[0]); + const transformer = new DecomposedMetadataTransformer(mockRegistry, new ConvertContext()); + + 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); + assert(false, 'expected TypeInferenceError to be thrown'); + } catch (err) { + expect(err).to.be.instanceOf(TypeInferenceError); + const msg = nls.localize('error_unexpected_child_type', [fsPath, component.type.name]); + expect(err.message).to.equal(msg); + } + }); + it('should defer write operations for children that are not members of merge component', async () => { const mergeComponentChild = component.getChildren()[0]; const { fullName, type } = component; diff --git a/test/convert/transformers/staticResourceMetadataTransformer.ts b/test/convert/transformers/staticResourceMetadataTransformer.ts index 18cbbf47e1..df5197c15c 100644 --- a/test/convert/transformers/staticResourceMetadataTransformer.ts +++ b/test/convert/transformers/staticResourceMetadataTransformer.ts @@ -109,6 +109,26 @@ describe('StaticResourceMetadataTransformer', () => { ); } }); + + it('should throw an error if content is directory but there is no resource file', async () => { + const component = SourceComponent.createVirtualComponent( + MIXED_CONTENT_DIRECTORY_COMPONENT, + MIXED_CONTENT_DIRECTORY_VIRTUAL_FS + ); + + // when there's no matching component.resource-meta.xml file + env.stub(component, 'parseXml').resolves({ StaticResource: undefined }); + + try { + await transformer.toMetadataFormat(component); + } catch (e) { + expect(e.message).to.equal( + nls.localize('error_static_resource_missing_resource_file', [ + join('staticresources', component.name), + ]) + ); + } + }); }); describe('toSourceFormat', () => { diff --git a/test/mock/client/transferOperations.ts b/test/mock/client/transferOperations.ts index b5510a6849..6aee0aec7f 100644 --- a/test/mock/client/transferOperations.ts +++ b/test/mock/client/transferOperations.ts @@ -258,6 +258,7 @@ export async function stubMetadataRetrieve( type: 'merge', mergeWith: retrievedComponents.getSourceComponents(), defaultDirectory: MOCK_DEFAULT_OUTPUT, + forceIgnoredPaths: retrievedComponents.forceIgnoredPaths ?? new Set(), }); converted = source; @@ -266,6 +267,7 @@ export async function stubMetadataRetrieve( type: 'merge', mergeWith: retrievedComponents.getSourceComponents(), defaultDirectory: pkg.outputDir, + forceIgnoredPaths: retrievedComponents.forceIgnoredPaths ?? new Set(), }) ); } else { @@ -297,7 +299,12 @@ export async function stubMetadataRetrieve( } const convertStub = sandbox.stub(MetadataConverter.prototype, 'convert'); outputConfigs.forEach((outputCfg) => { - convertStub.withArgs(match.any, 'source', outputCfg).resolves({ converted }); + const notForceIgnoredConverted = converted.filter( + (component) => !retrievedComponents.forceIgnoredPaths ?? [].includes(component.xml) + ); + convertStub + .withArgs(match.any, 'source', outputCfg) + .resolves({ converted: notForceIgnoredConverted }); }); return { diff --git a/test/resolve/adapters/decomposedSourceAdapter.ts b/test/resolve/adapters/decomposedSourceAdapter.ts index 1598c2fb1e..bf6b23dce9 100644 --- a/test/resolve/adapters/decomposedSourceAdapter.ts +++ b/test/resolve/adapters/decomposedSourceAdapter.ts @@ -11,12 +11,15 @@ import { decomposedtoplevel, mockRegistryData, xmlInFolder, + matchingContentFile, } from '../../mock/registry'; -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { VirtualTreeContainer, SourceComponent } from '../../../src'; import { RegistryTestUtil } from '../registryTestUtil'; import { join } from 'path'; import { META_XML_SUFFIX } from '../../../src/common'; +import { TypeInferenceError } from '../../../src/errors'; +import { nls } from '../../../src/i18n'; describe('DecomposedSourceAdapter', () => { const type = mockRegistryData.types.decomposed; @@ -123,4 +126,41 @@ describe('DecomposedSourceAdapter', () => { const adapter = new DefaultSourceAdapter(type, mockRegistry); expect(adapter.getComponent(path)).to.be.undefined; }); + + it('should throw an Error when unexpected child type found in parent folder', () => { + // This is most likely an odd project structure such as metadata found within a CustomObject + // folder that is not a child type of CustomObject. E.g., Layout, SharingRules, ApexClass. + // This test adds an ApexClass to the equivalent of here: + // .../main/default/objects/MyObject/classes/MyApexClass.cls-meta.xml + // The actual ApexClass file path for the test is: + // path/to/decomposeds/a/classes/a.mcf-meta.xml + const { CONTENT_NAMES, XML_NAMES } = matchingContentFile; + const fsUnexpectedChild = [ + { + dirPath: decomposed.DECOMPOSED_PATH, + children: [ + decomposed.DECOMPOSED_CHILD_XML_NAME_1, + decomposed.DECOMPOSED_CHILD_DIR, + 'classes', + ], + }, + { + dirPath: decomposed.DECOMPOSED_CHILD_DIR_PATH, + children: [decomposed.DECOMPOSED_CHILD_XML_NAME_2], + }, + { + dirPath: join(decomposed.DECOMPOSED_PATH, 'classes'), + children: [CONTENT_NAMES[0], XML_NAMES[0]], + }, + ]; + const tree = new VirtualTreeContainer(fsUnexpectedChild); + const adapter = new DecomposedSourceAdapter(type, mockRegistry, undefined, tree); + const fsPath = join(decomposed.DECOMPOSED_PATH, 'classes', XML_NAMES[0]); + + assert.throws( + () => adapter.getComponent(fsPath, false), + TypeInferenceError, + nls.localize('error_unexpected_child_type', [fsPath, type.name]) + ); + }); });