From 7525a73ab248bdbe6b1a18ce11838c5398e902c6 Mon Sep 17 00:00:00 2001 From: Bryan Powell Date: Fri, 20 Nov 2020 14:04:00 -0800 Subject: [PATCH] fix: handling folders during various operations (#208) --- scripts/update-registry/typeOverride.json | 5 + scripts/update-registry/update.js | 25 ++- src/collections/workingSet.ts | 35 +++- src/common/types.ts | 9 + src/convert/metadataConverter.ts | 11 +- .../defaultMetadataTransformer.ts | 16 +- src/metadata-registry/data/registry.json | 56 ++++-- test/collections/workingSet.test.ts | 189 +++++++++++------- test/convert/metadataConverter.test.ts | 14 +- .../defaultMetadataTransformer.ts | 18 +- test/mock/registry/index.ts | 6 +- test/mock/registry/tinaConstants.ts | 20 +- 12 files changed, 269 insertions(+), 135 deletions(-) diff --git a/scripts/update-registry/typeOverride.json b/scripts/update-registry/typeOverride.json index 665091c09a..4369a12981 100644 --- a/scripts/update-registry/typeOverride.json +++ b/scripts/update-registry/typeOverride.json @@ -1,5 +1,10 @@ { "emailservicesfunction": { "suffix": null + }, + "emailtemplatefolder": { + "id": "emailfolder", + "name": "EmailFolder", + "suffix": "emailFolder" } } diff --git a/scripts/update-registry/update.js b/scripts/update-registry/update.js index 1933cb396f..9d4ec9aa48 100644 --- a/scripts/update-registry/update.js +++ b/scripts/update-registry/update.js @@ -32,31 +32,40 @@ function update(registry, describeResult) { const typeOverrides = JSON.parse(fs.readFileSync(path.join(__dirname, 'typeOverride.json'))) for (const object of describeResult.metadataObjects) { - const typeId = object.xmlName.toLowerCase(); - const { xmlName: name, suffix, directoryName, inFolder, childXmlNames } = object; + let typeId = object.xmlName.toLowerCase(); + const { xmlName: name, suffix, directoryName, inFolder, childXmlNames, folderContentType } = object; // If it's a type with folders, process the folder type later - if (inFolder === 'true') { + let folderTypeId; + if (inFolder === 'true' || inFolder === true) { describeResult.metadataObjects.push({ xmlName: `${name}Folder`, suffix: `${typeId}Folder`, directoryName, - inFolder: false, + folderContentType: typeId }); + folderTypeId = `${typeId}folder` } - let type = registry.types[typeId] || { + const generatedType = { id: typeId, name, suffix, directoryName, inFolder: inFolder === 'true' || inFolder === true, - strictDirectoryName: !suffix + strictDirectoryName: !suffix, + folderType: folderTypeId, + folderContentType }; + let type = deepmerge(generatedType, registry.types[typeId] || {}) // apply type override if one exists - if (typeOverrides[typeId]) { - type = deepmerge(type, typeOverrides[typeId]) + const typeOverride = typeOverrides[typeId] + if (typeOverride) { + type = deepmerge(type, typeOverride) + if (typeOverride.id) { + typeId = typeOverride.id + } } if (childXmlNames) { diff --git a/src/collections/workingSet.ts b/src/collections/workingSet.ts index 8e1f56d600..98a202154f 100644 --- a/src/collections/workingSet.ts +++ b/src/collections/workingSet.ts @@ -57,7 +57,7 @@ export class WorkingSet implements MetadataSet, Iterable { * with the `resolve` option to resolve source files for the components. * * ``` - * WorkingSet.fromManifest('/path/to/package.xml', { + * WorkingSet.fromManifestFile('/path/to/package.xml', { * resolve: '/path/to/force-app' * }); * ``` @@ -128,9 +128,14 @@ export class WorkingSet implements MetadataSet, Iterable { for (const { name: typeName, members } of typeMembers) { const fullNames = Array.isArray(members) ? members : [members]; for (const fullName of fullNames) { + let type = registry.getTypeByName(typeName); + // if there is no delimeter and it's a type in folders, infer folder component + if (type.folderType && !fullName.includes('/')) { + type = registry.getTypeByName(type.folderType); + } yield { fullName, - type: registry.getTypeByName(typeName), + type, }; } } @@ -209,12 +214,34 @@ export class WorkingSet implements MetadataSet, Iterable { */ public getObject(): PackageManifestObject { const typeMembers: PackageTypeMembers[] = []; + const folderMembers = new Map(); for (const [typeName, components] of this.components.entries()) { - const members: string[] = []; + let members: string[] = []; + const type = this.registry.getTypeByName(typeName); + + // build folder related members separately to combine folder types and types in folders into one + const isFolderRelatedType = type.folderType || type.folderContentType; + if (isFolderRelatedType) { + const { name: contentTypeName } = type.folderContentType + ? this.registry.getTypeByName(type.folderContentType) + : type; + if (!folderMembers.has(contentTypeName)) { + folderMembers.set(contentTypeName, []); + } + members = folderMembers.get(contentTypeName); + } + for (const { fullName } of components.values()) { members.push(fullName); } + + if (!isFolderRelatedType) { + typeMembers.push({ members, name: typeName }); + } + } + + for (const [typeName, members] of folderMembers.entries()) { typeMembers.push({ members, name: typeName }); } @@ -343,6 +370,6 @@ export class WorkingSet implements MetadataSet, Iterable { if (!this.components.has(type.name)) { this.components.set(type.name, new ComponentSet()); } - this.components.get(type.name).add(Object.freeze(component)); + this.components.get(type.name).add(component); } } diff --git a/src/common/types.ts b/src/common/types.ts index a5e52cc06a..b40f7222ae 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -23,6 +23,7 @@ export type MetadataType = { * Whether or not components are stored in folders. * * __Examples:__ Reports, Dashboards, Documents, EmailTemplates + * @deprecated use `folderType` to get the related folder type, if one exists */ inFolder?: boolean; /** @@ -37,6 +38,14 @@ export type MetadataType = { * Whether or not components are required to reside in a folder named after the type's directoryName. */ strictDirectoryName?: boolean; + /** + * If the type is a folder type (container for components), the id of the type it is a container for. + */ + folderContentType?: string; + /** + * If the type is contained in folders, the id of the type that contains it. + */ + folderType?: string; /** * Type definitions for child types, if the type has any. * diff --git a/src/convert/metadataConverter.ts b/src/convert/metadataConverter.ts index 55f896f5a8..d6398c81bf 100644 --- a/src/convert/metadataConverter.ts +++ b/src/convert/metadataConverter.ts @@ -11,7 +11,7 @@ import { DirectoryConfig, ZipConfig, } from './types'; -import { ManifestGenerator, RegistryAccess, SourceComponent } from '../metadata-registry'; +import { RegistryAccess, SourceComponent } from '../metadata-registry'; import { promises } from 'fs'; import { dirname, join } from 'path'; import { ensureDirectoryExists } from '../utils/fileSystemHandler'; @@ -25,7 +25,7 @@ import { } from './streams'; import { ConversionError, LibraryError } from '../errors'; import { SourcePath } from '../common'; -import { ComponentSet } from '../collections'; +import { ComponentSet, WorkingSet } from '../collections'; export class MetadataConverter { public static readonly PACKAGE_XML_FILE = 'package.xml'; @@ -50,9 +50,10 @@ export class MetadataConverter { output: ConvertOutputConfig ): Promise { try { - // TODO: evaluate if a builder pattern for manifest creation is more efficient here - const manifestGenerator = new ManifestGenerator(undefined, this.registry); - const manifestContents = manifestGenerator.createManifest(components); + // it's possible the components came from a working set, so this may be redundant in some cases... + const manifestContents = WorkingSet.fromComponents(components, { + registry: this.registry, + }).getPackageXml(); const isSource = targetFormat === 'source'; const tasks = []; diff --git a/src/convert/transformers/defaultMetadataTransformer.ts b/src/convert/transformers/defaultMetadataTransformer.ts index dd8a75e563..8112a5550b 100644 --- a/src/convert/transformers/defaultMetadataTransformer.ts +++ b/src/convert/transformers/defaultMetadataTransformer.ts @@ -85,12 +85,18 @@ export class DefaultMetadataTransformer extends BaseMetadataTransformer { let xmlDestination = component.getPackageRelativePath(component.xml, targetFormat); - // quirk: append or strip the -meta.xml suffix to the xml if there's no content + // quirks: + // - append or strip the -meta.xml suffix to the path if there's no content + // - remove file extension but preserve -meta.xml suffix if folder type if (!component.content) { - xmlDestination = - targetFormat === 'metadata' - ? xmlDestination.slice(0, xmlDestination.lastIndexOf(META_XML_SUFFIX)) - : `${xmlDestination}${META_XML_SUFFIX}`; + if (targetFormat === 'metadata') { + const { folderContentType, suffix } = component.type; + xmlDestination = folderContentType + ? xmlDestination.replace(`.${suffix}`, '') + : xmlDestination.slice(0, xmlDestination.lastIndexOf(META_XML_SUFFIX)); + } else { + xmlDestination = `${xmlDestination}${META_XML_SUFFIX}`; + } } return xmlDestination; diff --git a/src/metadata-registry/data/registry.json b/src/metadata-registry/data/registry.json index 23344070a5..7f316d8280 100644 --- a/src/metadata-registry/data/registry.json +++ b/src/metadata-registry/data/registry.json @@ -14,6 +14,7 @@ "suffix": "labels", "directoryName": "labels", "inFolder": false, + "strictDirectoryName": false, "children": { "types": { "customlabel": { @@ -29,8 +30,7 @@ "directories": { "customLabels": "customlabel" } - }, - "strictDirectoryName": false + } }, "navigationmenu": { "id": "navigationmenu", @@ -352,7 +352,8 @@ "suffix": "report", "directoryName": "reports", "inFolder": true, - "strictDirectoryName": false + "strictDirectoryName": false, + "folderType": "reportfolder" }, "dashboard": { "id": "dashboard", @@ -360,7 +361,8 @@ "suffix": "dashboard", "directoryName": "dashboards", "inFolder": true, - "strictDirectoryName": false + "strictDirectoryName": false, + "folderType": "dashboardfolder" }, "visualizationplugin": { "id": "visualizationplugin", @@ -398,6 +400,7 @@ "directoryName": "documents", "inFolder": true, "strictDirectoryName": true, + "folderType": "documentfolder", "strategies": { "adapter": "mixedContent" } @@ -425,6 +428,7 @@ "directoryName": "email", "inFolder": true, "strictDirectoryName": false, + "folderType": "emailfolder", "strategies": { "adapter": "matchingContentFile" } @@ -575,6 +579,7 @@ "suffix": "workflow", "directoryName": "workflows", "inFolder": false, + "strictDirectoryName": false, "children": { "types": { "workflowfieldupdate": { @@ -637,8 +642,7 @@ "workflowOutboundMessages": "workflowoutboundmessage", "workflowRules": "workflowrule" } - }, - "strictDirectoryName": false + } }, "assignmentrules": { "id": "assignmentrules", @@ -646,6 +650,7 @@ "suffix": "assignmentRules", "directoryName": "assignmentRules", "inFolder": false, + "strictDirectoryName": false, "children": { "types": { "assignmentrule": { @@ -661,8 +666,7 @@ "directories": { "assignmentRules": "assignmentrule" } - }, - "strictDirectoryName": false + } }, "autoresponserules": { "id": "autoresponserules", @@ -670,6 +674,7 @@ "suffix": "autoResponseRules", "directoryName": "autoResponseRules", "inFolder": false, + "strictDirectoryName": false, "children": { "types": { "autoresponserule": { @@ -685,8 +690,7 @@ "directories": { "autoResponseRules": "autoresponserule" } - }, - "strictDirectoryName": false + } }, "escalationrules": { "id": "escalationrules", @@ -694,6 +698,7 @@ "suffix": "escalationRules", "directoryName": "escalationRules", "inFolder": false, + "strictDirectoryName": false, "children": { "types": { "escalationrule": { @@ -709,8 +714,7 @@ "directories": { "escalationRules": "escalationrule" } - }, - "strictDirectoryName": false + } }, "posttemplate": { "id": "posttemplate", @@ -904,6 +908,7 @@ "suffix": "matchingRule", "directoryName": "matchingRules", "inFolder": false, + "strictDirectoryName": false, "children": { "types": { "matchingrule": { @@ -919,8 +924,7 @@ "directories": { "matchingRules": "matchingrule" } - }, - "strictDirectoryName": false + } }, "duplicaterule": { "id": "duplicaterule", @@ -1087,6 +1091,7 @@ "suffix": "sharingRules", "directoryName": "sharingRules", "inFolder": false, + "strictDirectoryName": false, "children": { "types": { "sharingownerrule": { @@ -1124,8 +1129,7 @@ "sharingOwnerRules": "sharingownerrule", "sharingCriteriaRules": "sharingcriteriarule" } - }, - "strictDirectoryName": false + } }, "sharingset": { "id": "sharingset", @@ -1703,28 +1707,36 @@ "name": "ReportFolder", "suffix": "reportFolder", "directoryName": "reports", - "inFolder": false + "inFolder": false, + "strictDirectoryName": false, + "folderContentType": "report" }, "dashboardfolder": { "id": "dashboardfolder", "name": "DashboardFolder", "suffix": "dashboardFolder", "directoryName": "dashboards", - "inFolder": false + "inFolder": false, + "strictDirectoryName": false, + "folderContentType": "dashboard" }, "documentfolder": { "id": "documentfolder", "name": "DocumentFolder", "suffix": "documentFolder", "directoryName": "documents", - "inFolder": false + "inFolder": false, + "strictDirectoryName": false, + "folderContentType": "document" }, "emailfolder": { "id": "emailfolder", "name": "EmailFolder", "suffix": "emailFolder", "directoryName": "email", - "inFolder": false + "inFolder": false, + "strictDirectoryName": false, + "folderContentType": "emailtemplate" }, "inboundnetworkconnection": { "id": "inboundnetworkconnection", @@ -2034,7 +2046,7 @@ "reportFolder": "reportfolder", "dashboardFolder": "dashboardfolder", "documentFolder": "documentfolder", - "emailFolder": "emailFolder", + "emailFolder": "emailfolder", "inboundNetworkConnection": "inboundnetworkconnection", "outboundNetworkConnection": "outboundnetworkconnection", "mutingpermissionset": "mutingpermissionset", @@ -2091,4 +2103,4 @@ "botversion": "bot" }, "apiVersion": "50.0" -} +} \ No newline at end of file diff --git a/test/collections/workingSet.test.ts b/test/collections/workingSet.test.ts index bff85756b6..fa8e65d77e 100644 --- a/test/collections/workingSet.test.ts +++ b/test/collections/workingSet.test.ts @@ -31,8 +31,8 @@ import { mockRegistry, mockRegistryData } from '../mock/registry'; const env = createSandbox(); -const packageXml: VirtualFile = { - name: 'package.xml', +const subsetXml: VirtualFile = { + name: 'subset.xml', data: Buffer.from(` @@ -47,8 +47,8 @@ const packageXml: VirtualFile = { \n`), }; -const packageXmlComplete: VirtualFile = { - name: 'packageComplete.xml', +const completeXml: VirtualFile = { + name: 'complete.xml', data: Buffer.from(` @@ -64,8 +64,8 @@ const packageXmlComplete: VirtualFile = { \n`), }; -const packageXmlWildcard: VirtualFile = { - name: 'packageWildcard.xml', +const wildcardXml: VirtualFile = { + name: 'wildcard.xml', data: Buffer.from(` @@ -76,8 +76,8 @@ const packageXmlWildcard: VirtualFile = { \n`), }; -const packageXmlSingleMember: VirtualFile = { - name: 'packageSingleMember.xml', +const singleMemberXml: VirtualFile = { + name: 'singleMember.xml', data: Buffer.from(` @@ -88,15 +88,28 @@ const packageXmlSingleMember: VirtualFile = { \n`), }; +const folderComponentXml: VirtualFile = { + name: 'folderComponent.xml', + data: Buffer.from(` + + + Test_Folder + TinaFey + + ${mockRegistry.apiVersion} +\n`), +}; + const virtualPackageFiles: VirtualDirectory[] = [ { dirPath: '.', children: [ 'decomposedTopLevels', 'mixedSingleFiles', - packageXml, - packageXmlWildcard, - packageXmlSingleMember, + subsetXml, + wildcardXml, + singleMemberXml, + folderComponentXml, ], }, { @@ -121,19 +134,19 @@ describe('WorkingSet', () => { describe('Initializers', () => { describe('fromSource', () => { it('should initialize with source backed components', () => { - const mdp = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); + const ws = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); const expected = new MetadataResolver(mockRegistry, tree).getComponentsFromPath('.'); - expect(Array.from(mdp)).to.deep.equal(expected); + expect(Array.from(ws)).to.deep.equal(expected); }); }); describe('fromManifestFile', () => { it('should not initialize with source backed components by default', async () => { - const mdp = await WorkingSet.fromManifestFile('package.xml', { + const ws = await WorkingSet.fromManifestFile('subset.xml', { registry: mockRegistry, tree, }); - expect(Array.from(mdp)).to.deep.equal([ + expect(Array.from(ws)).to.deep.equal([ { fullName: 'a', type: mockRegistryData.types.decomposedtoplevel, @@ -149,11 +162,11 @@ describe('WorkingSet', () => { * xml parsing library returns string | string[] for entries, tests that this is handled */ it('should handle types with one member properly', async () => { - const mdp = await WorkingSet.fromManifestFile('packageSingleMember.xml', { + const ws = await WorkingSet.fromManifestFile('singleMember.xml', { registry: mockRegistry, tree, }); - expect(Array.from(mdp)).to.deep.equal([ + expect(Array.from(ws)).to.deep.equal([ { fullName: 'Test', type: mockRegistryData.types.mixedcontentsinglefile, @@ -161,8 +174,21 @@ describe('WorkingSet', () => { ]); }); + it('should interpret a member of a type in folders with no delimeter as its corresponding folder type', async () => { + const ws = await WorkingSet.fromManifestFile('folderComponent.xml', { + registry: mockRegistry, + tree, + }); + expect(Array.from(ws)).to.deep.equal([ + { + fullName: 'Test_Folder', + type: mockRegistryData.types.tinafeyfolder, + }, + ]); + }); + it('should initialize with source backed components when specifying resolve option', async () => { - const mdp = await WorkingSet.fromManifestFile('package.xml', { + const ws = await WorkingSet.fromManifestFile('subset.xml', { registry: mockRegistry, tree, resolve: '.', @@ -172,30 +198,30 @@ describe('WorkingSet', () => { const missingIndex = expected.findIndex((c) => c.fullName === 'c'); expected.splice(missingIndex, 1); - expect(Array.from(mdp)).to.deep.equal(expected); + expect(Array.from(ws)).to.deep.equal(expected); }); it('should interpret wildcard members literally by default', async () => { - const mdp = await WorkingSet.fromManifestFile('packageWildcard.xml', { + const ws = await WorkingSet.fromManifestFile('wildcard.xml', { registry: mockRegistry, tree, }); - expect(mdp.has({ fullName: '*', type: 'MixedContentSingleFile' })).to.be.true; + expect(ws.has({ fullName: '*', type: 'MixedContentSingleFile' })).to.be.true; }); it('should interpret wildcard members literally when literalWildcard = true', async () => { - const mdp = await WorkingSet.fromManifestFile('packageWildcard.xml', { + const ws = await WorkingSet.fromManifestFile('wildcard.xml', { registry: mockRegistry, tree, literalWildcard: true, }); - expect(mdp.has({ fullName: '*', type: 'MixedContentSingleFile' })); + expect(ws.has({ fullName: '*', type: 'MixedContentSingleFile' })); }); it('should resolve components when literalWildcard = false and wildcard is encountered', async () => { - const mdp = await WorkingSet.fromManifestFile('packageWildcard.xml', { + const ws = await WorkingSet.fromManifestFile('wildcard.xml', { registry: mockRegistry, tree, resolve: '.', @@ -205,11 +231,11 @@ describe('WorkingSet', () => { 'mixedSingleFiles' ); - expect(Array.from(mdp)).to.deep.equal(expected); + expect(Array.from(ws)).to.deep.equal(expected); }); it('should resolve components and add literal wildcard component when literalWildcard = true and resolve != undefined', async () => { - const mdp = await WorkingSet.fromManifestFile('packageWildcard.xml', { + const ws = await WorkingSet.fromManifestFile('wildcard.xml', { registry: mockRegistry, tree, resolve: '.', @@ -219,7 +245,7 @@ describe('WorkingSet', () => { 'mixedSingleFiles' ); - expect(Array.from(mdp)).to.deep.equal([ + expect(Array.from(ws)).to.deep.equal([ { fullName: '*', type: mockRegistryData.types.mixedcontentsinglefile }, ...sourceComponents, ]); @@ -228,7 +254,7 @@ describe('WorkingSet', () => { describe('fromComponents', () => { it('should initialize non-source backed components from members', () => { - const mdp = WorkingSet.fromComponents( + const ws = WorkingSet.fromComponents( [ { fullName: 'Test1', @@ -242,7 +268,7 @@ describe('WorkingSet', () => { { registry: mockRegistry } ); - expect(Array.from(mdp)).to.deep.equal([ + expect(Array.from(ws)).to.deep.equal([ { fullName: 'Test1', type: mockRegistryData.types.decomposedtoplevel, @@ -258,8 +284,8 @@ describe('WorkingSet', () => { describe('getObject', () => { it('should return an object representing the package manifest', () => { - const mdp = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); - expect(mdp.getObject()).to.deep.equal({ + const ws = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); + expect(ws.getObject()).to.deep.equal({ Package: { types: [ { @@ -275,20 +301,33 @@ describe('WorkingSet', () => { }, }); }); + + it('should interpret folder components as members of the type they are a container for', () => { + const member = { fullName: 'Test_Folder', type: 'TinaFeyFolder' }; + const ws = WorkingSet.fromComponents([member], { registry: mockRegistry }); + + expect(ws.has(member)).to.be.true; + expect(ws.getObject().Package.types).to.deep.equal([ + { + name: 'TinaFey', + members: ['Test_Folder'], + }, + ]); + }); }); describe('resolveSourceComponents', () => { it('should resolve components and add to package', () => { - const mdp = new WorkingSet(mockRegistry); + const ws = new WorkingSet(mockRegistry); const expected = new MetadataResolver(mockRegistry, tree).getComponentsFromPath('.'); - const result = mdp.resolveSourceComponents('.', { tree }); + const result = ws.resolveSourceComponents('.', { tree }); expect(Array.from(result)).to.deep.equal(expected); - expect(Array.from(mdp)).to.deep.equal(expected); + expect(Array.from(ws)).to.deep.equal(expected); }); it('should resolve components and filter', async () => { - const mdp = new WorkingSet(mockRegistry); + const ws = new WorkingSet(mockRegistry); const filter = [{ fullName: 'b', type: mockRegistryData.types.mixedcontentsinglefile }]; const expected = new MetadataResolver(mockRegistry, tree).getComponentsFromPath('.'); @@ -301,10 +340,10 @@ describe('WorkingSet', () => { 1 ); - const result = mdp.resolveSourceComponents('.', { tree, filter }); + const result = ws.resolveSourceComponents('.', { tree, filter }); expect(Array.from(result)).to.deep.equal(expected); - expect(Array.from(mdp)).to.deep.equal(expected); + expect(Array.from(ws)).to.deep.equal(expected); }); it('should only resolve child components when present in filter even if parent source exists', () => { @@ -318,30 +357,30 @@ describe('WorkingSet', () => { type: mockRegistryData.types.decomposedtoplevel.children.types.g, }, ]; - const mdp = new WorkingSet(mockRegistry); - const result = mdp.resolveSourceComponents('.', { tree, filter }); + const ws = new WorkingSet(mockRegistry); + const result = ws.resolveSourceComponents('.', { tree, filter }); const expected = new MetadataResolver(mockRegistry, tree) .getComponentsFromPath('decomposedTopLevels')[0] .getChildren(); expect(Array.from(result)).to.deep.equal(expected); - expect(Array.from(mdp)).to.deep.equal(expected); + expect(Array.from(ws)).to.deep.equal(expected); }); }); describe('getPackageXml', () => { it('should return manifest string when initialized from manifest file', async () => { - const mdp = await WorkingSet.fromManifestFile('package.xml', { + const ws = await WorkingSet.fromManifestFile('subset.xml', { registry: mockRegistry, tree, }); - expect(mdp.getPackageXml()).to.equal(packageXml.data.toString()); + expect(ws.getPackageXml()).to.equal(subsetXml.data.toString()); }); it('should return manifest string when initialized from source', () => { - const mdp = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); - expect(mdp.getPackageXml(4)).to.equal(packageXmlComplete.data.toString()); + const ws = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); + expect(ws.getPackageXml(4)).to.equal(completeXml.data.toString()); }); }); @@ -359,11 +398,11 @@ describe('WorkingSet', () => { username: 'test@foobar.com', }), }); - const mdp = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); + const ws = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); const deployStub = env.stub(MetadataApi.prototype, 'deploy'); - deployStub.withArgs(Array.from(mdp) as SourceComponent[]).resolves(mockResult); + deployStub.withArgs(Array.from(ws) as SourceComponent[]).resolves(mockResult); - const result = await mdp.deploy(mockConnection); + const result = await ws.deploy(mockConnection); expect(result).to.deep.equal(mockResult); }); @@ -371,12 +410,12 @@ describe('WorkingSet', () => { it('should warn when some components are missing source', async () => { stub(MetadataApi.prototype, 'deploy'); const warnStub = env.stub(console, 'warn').callsFake(() => true); - const mdp = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); - const missing = Array.from(mdp).map((c) => `${c.type.name}:${c.fullName}`); + const ws = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); + const missing = Array.from(ws).map((c) => `${c.type.name}:${c.fullName}`); - mdp.add({ fullName: 'NoSource', type: 'MixedContentSingleFile' }); + ws.add({ fullName: 'NoSource', type: 'MixedContentSingleFile' }); - await mdp.deploy('test@foobar.com'); + await ws.deploy('test@foobar.com'); expect( warnStub.calledOnceWith( @@ -386,13 +425,13 @@ describe('WorkingSet', () => { }); it('should throw error if there are no source backed components when deploying', async () => { - const mdp = await WorkingSet.fromManifestFile('package.xml', { + const ws = await WorkingSet.fromManifestFile('subset.xml', { registry: mockRegistry, tree, }); try { - await mdp.deploy('test@foobar.com'); + await ws.deploy('test@foobar.com'); fail('should have thrown an error'); } catch (e) { expect(e.name).to.equal(WorkingSetError.name); @@ -401,13 +440,13 @@ describe('WorkingSet', () => { }); it('should throw error if attempting to deploy a wildcard literal component', async () => { - const mdp = await WorkingSet.fromManifestFile('packageWildcard.xml', { + const ws = await WorkingSet.fromManifestFile('wildcard.xml', { registry: mockRegistry, tree, }); try { - await mdp.deploy('test@foobar.com'); + await ws.deploy('test@foobar.com'); fail('should have thrown an error'); } catch (e) { expect(e.name).to.equal(WorkingSetError.name); @@ -430,18 +469,18 @@ describe('WorkingSet', () => { username: 'test@foobar.com', }), }); - const mdp = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); + const ws = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); const retrieveStub = env.stub(MetadataApi.prototype, 'retrieve'); retrieveStub .withArgs({ - components: Array.from(mdp) as SourceComponent[], + components: Array.from(ws) as SourceComponent[], merge: undefined, output: '/test/path', wait: undefined, }) .resolves(mockResult); - const result = await mdp.retrieve(mockConnection, '/test/path'); + const result = await ws.retrieve(mockConnection, '/test/path'); expect(result).to.deep.equal(mockResult); }); @@ -452,17 +491,17 @@ describe('WorkingSet', () => { username: 'test@foobar.com', }), }); - const mdp = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); + const ws = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); const retrieveStub = stub(MetadataApi.prototype, 'retrieve'); - await mdp.retrieve(mockConnection, '/test/path', { + await ws.retrieve(mockConnection, '/test/path', { merge: true, wait: 1234, }); expect( retrieveStub.calledWith({ - components: Array.from(mdp) as SourceComponent[], + components: Array.from(ws) as SourceComponent[], merge: true, output: '/test/path', wait: 1234, @@ -471,9 +510,9 @@ describe('WorkingSet', () => { }); it('should throw error if there are no components when retrieving', async () => { - const mdp = new WorkingSet(mockRegistry); + const ws = new WorkingSet(mockRegistry); try { - await mdp.retrieve('test@foobar.com', '/test/path'); + await ws.retrieve('test@foobar.com', '/test/path'); fail('should have thrown an error'); } catch (e) { expect(e.name).to.equal(WorkingSetError.name); @@ -484,13 +523,13 @@ describe('WorkingSet', () => { describe('add', () => { it('should add metadata member to package components', async () => { - const mdp = new WorkingSet(mockRegistry); + const ws = new WorkingSet(mockRegistry); - expect(mdp.size).to.equal(0); + expect(ws.size).to.equal(0); - mdp.add({ fullName: 'foo', type: 'DecomposedTopLevel' }); + ws.add({ fullName: 'foo', type: 'DecomposedTopLevel' }); - expect(Array.from(mdp)).to.deep.equal([ + expect(Array.from(ws)).to.deep.equal([ { fullName: 'foo', type: mockRegistryData.types.decomposedtoplevel, @@ -499,14 +538,14 @@ describe('WorkingSet', () => { }); it('should add metadata component to package components', async () => { - const mdp = new WorkingSet(mockRegistry); + const ws = new WorkingSet(mockRegistry); const component = { fullName: 'bar', type: mockRegistryData.types.mixedcontentsinglefile }; - expect(mdp.size).to.equal(0); + expect(ws.size).to.equal(0); - mdp.add(component); + ws.add(component); - expect(Array.from(mdp)).to.deep.equal([component]); + expect(Array.from(ws)).to.deep.equal([component]); }); }); @@ -548,7 +587,7 @@ describe('WorkingSet', () => { describe('entries', () => { it('should return component entries of the set by type name', () => { - const mdp = WorkingSet.fromComponents( + const ws = WorkingSet.fromComponents( [ { fullName: 'Test1', @@ -566,7 +605,7 @@ describe('WorkingSet', () => { { registry: mockRegistry } ); - const entries = Array.from(mdp.entries()); + const entries = Array.from(ws.entries()); expect(entries.length).to.equal(2); const dtls = entries.find((entry) => entry[0] === 'DecomposedTopLevel')[1]; @@ -592,7 +631,7 @@ describe('WorkingSet', () => { }); it('should calculate size correctly', () => { - const mdp = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); - expect(mdp.size).to.equal(3); + const ws = WorkingSet.fromSource('.', { registry: mockRegistry, tree }); + expect(ws.size).to.equal(3); }); }); diff --git a/test/convert/metadataConverter.test.ts b/test/convert/metadataConverter.test.ts index 1211a2116d..11f43dfdc5 100644 --- a/test/convert/metadataConverter.test.ts +++ b/test/convert/metadataConverter.test.ts @@ -16,7 +16,7 @@ import { expect, assert } from 'chai'; import { ConversionError, LibraryError } from '../../src/errors'; import { TINA_COMPONENTS } from '../mock/registry/tinaConstants'; import { fail } from 'assert'; -import { ComponentSet } from '../../src'; +import { ComponentSet, WorkingSet } from '../../src'; const env = createSandbox(); @@ -124,9 +124,9 @@ describe('MetadataConverter', () => { `${MetadataConverter.DEFAULT_PACKAGE_PREFIX}_${timestamp}` ); env.stub(Date, 'now').returns(timestamp); - const expectedContents = new ManifestGenerator(undefined, mockRegistry).createManifest( - components - ); + const expectedContents = WorkingSet.fromComponents(components, { + registry: mockRegistry, + }).getPackageXml(); await converter.convert(components, 'metadata', { type: 'directory', outputDirectory }); @@ -200,9 +200,9 @@ describe('MetadataConverter', () => { }); it('should write manifest for metadata format conversion', async () => { - const expectedContents = new ManifestGenerator(undefined, mockRegistry).createManifest( - components - ); + const expectedContents = WorkingSet.fromComponents(components, { + registry: mockRegistry, + }).getPackageXml(); const addToZipStub = env.stub(streams.ZipWriter.prototype, 'addToZip'); await converter.convert(components, 'metadata', { type: 'zip' }); diff --git a/test/convert/transformers/defaultMetadataTransformer.ts b/test/convert/transformers/defaultMetadataTransformer.ts index de77846aa5..fa8a469224 100644 --- a/test/convert/transformers/defaultMetadataTransformer.ts +++ b/test/convert/transformers/defaultMetadataTransformer.ts @@ -14,6 +14,7 @@ import { expect } from 'chai'; import { DEFAULT_PACKAGE_ROOT_SFDX, META_XML_SUFFIX } from '../../../src/common'; import { SourceComponent, VirtualTreeContainer } from '../../../src'; import { GENE_COMPONENT, GENE_XML_NAME } from '../../mock/registry/geneConstants'; +import { TINA_FOLDER_COMPONENT } from '../../mock/registry/tinaConstants'; const env = createSandbox(); @@ -69,7 +70,7 @@ describe('DefaultMetadataTransformer', () => { }); }); - it('should handle folder type components with no content', async () => { + it('should remove the -meta.xml suffix for components with no content and in folders', async () => { const component = SourceComponent.createVirtualComponent(kathy.KATHY_COMPONENTS[0], []); const fullNameParts = component.fullName.split('/'); const { directoryName } = component.type; @@ -89,6 +90,21 @@ describe('DefaultMetadataTransformer', () => { writeInfos: expectedInfos, }); }); + + it('should remove file extension and preserve -meta.xml for folder components', async () => { + const component = TINA_FOLDER_COMPONENT; + const expectedInfos: WriteInfo[] = [ + { + output: join(component.type.directoryName, `${component.fullName}${META_XML_SUFFIX}`), + source: component.tree.stream(component.xml), + }, + ]; + + expect(await transformer.toMetadataFormat(component)).to.deep.equal({ + component, + writeInfos: expectedInfos, + }); + }); }); describe('toSourceFormat', () => { diff --git a/test/mock/registry/index.ts b/test/mock/registry/index.ts index 376afe44c9..47f1f74c25 100644 --- a/test/mock/registry/index.ts +++ b/test/mock/registry/index.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 { join } from 'path'; -import { RegistryAccess } from '../../../src/metadata-registry'; +import { MetadataRegistry, RegistryAccess } from '../../../src/metadata-registry'; export const mockRegistryData = { types: { @@ -33,6 +33,7 @@ export const mockRegistryData = { inFolder: true, name: 'TinaFey', strictDirectoryName: true, + folderType: 'tinafeyfolder', strategies: { adapter: 'mixedContent', }, @@ -74,6 +75,7 @@ export const mockRegistryData = { inFolder: false, name: 'TinaFeyFolder', suffix: 'tinafeyFolder', + folderContentType: 'tinafey', }, genewilder: { id: 'genewilder', @@ -206,7 +208,7 @@ export const mockRegistryData = { badchildtype: 'mixedcontentsinglefile', }, apiVersion: '48.0', -}; +} as MetadataRegistry; export const mockRegistry = new RegistryAccess(mockRegistryData); diff --git a/test/mock/registry/tinaConstants.ts b/test/mock/registry/tinaConstants.ts index d5f5e07317..fa3b9c1a0a 100644 --- a/test/mock/registry/tinaConstants.ts +++ b/test/mock/registry/tinaConstants.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 */ -import { join } from 'path'; +import { basename, join } from 'path'; import { mockRegistryData } from '.'; import { SourceComponent } from '../../../src'; @@ -18,11 +18,19 @@ export const TINA_XML_NAMES = ['a.tina-meta.xml', 'b.tina-meta.xml']; export const TINA_XML_PATHS = TINA_XML_NAMES.map((n) => join(TINA_FOLDER, n)); export const TINA_SOURCE_NAMES = ['a.x', 'b.y']; export const TINA_SOURCE_PATHS = TINA_SOURCE_NAMES.map((n) => join(TINA_FOLDER, n)); -export const TINA_FOLDER_COMPONENT = new SourceComponent({ - name: 'A_Folder', - type: mockRegistryData.types.tinafeyfolder, - xml: TINA_FOLDER_XML, -}); +export const TINA_FOLDER_COMPONENT = SourceComponent.createVirtualComponent( + { + name: 'A_Folder', + type: mockRegistryData.types.tinafeyfolder, + xml: TINA_FOLDER_XML, + }, + [ + { + dirPath: TINA_FOLDER, + children: [basename(TINA_FOLDER_XML)], + }, + ] +); export const TINA_COMPONENTS = [ new SourceComponent({ name: 'A_Folder/a',