Skip to content

feat: no adapter classes #1382

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
63ce40e
fix(perf): fewer adapter classes
mshanemc Jun 25, 2024
fd40840
refactor: derive metadataWithContent directly from type, not type => …
mshanemc Jun 25, 2024
70cf34c
test: remove invalid test case
mshanemc Jun 25, 2024
3095134
chore: bump core for xnuts
mshanemc Jun 25, 2024
08371d0
wip: clean up existing classes
mshanemc Jun 25, 2024
5b6d0ac
wip: even less classy adapters
mshanemc Jun 27, 2024
2ebc8de
Merge remote-tracking branch 'origin/main' into sm/no-adapter-classes
mshanemc Jun 30, 2024
f69a337
docs: typo
mshanemc Jul 15, 2024
e4414d0
refactor: no adapter classes
mshanemc Jul 15, 2024
018257d
Merge remote-tracking branch 'origin/main' into sm/no-adapter-classes
mshanemc Jul 26, 2024
496a493
refactor: organize adapter shared types
mshanemc Jul 26, 2024
d542496
chore: rename deb bundle detector
mshanemc Jul 30, 2024
3b93cc4
fix: get deb to resolve properly
mshanemc Jul 31, 2024
61b779d
chore: stash wip
mshanemc Jul 31, 2024
e0b80bb
test: set the contents for a forceIgnore. handy for ut
mshanemc Aug 1, 2024
5a0ff3c
test: passing ut and snapshot
mshanemc Aug 1, 2024
1727cd3
Merge remote-tracking branch 'origin/main' into sm/no-adapter-classes
mshanemc Aug 1, 2024
fffaea9
fix: allow more undefined from adapters
mshanemc Aug 1, 2024
42eb6d7
refactor: shared posixify fn for windows ut
mshanemc Aug 1, 2024
6e91e38
docs: handbook update
mshanemc Aug 1, 2024
0f149cc
docs: remove wip readme for adapters
mshanemc Aug 1, 2024
78ba78c
test: windows ut need posix paths in forceignore contents
mshanemc Aug 2, 2024
1ff3f66
test: ut for forceignore injection
mshanemc Aug 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test: passing ut and snapshot
  • Loading branch information
mshanemc committed Aug 1, 2024

Verified

This commit was signed with the committer’s verified signature.
mshanemc Shane McLaughlin
commit 5a0ff3cd9c3610fcc73535365beb96e597d839ac
13 changes: 10 additions & 3 deletions src/resolve/adapters/bundleSourceAdapter.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { basename } from 'node:path';
import { ensure } from '@salesforce/ts-types';
import { parseMetadataXml } from '../../utils';
import { getComponent, parseAsRootMetadataXml, trimPathToContent } from './baseSourceAdapter';
import { MaybeGetComponent } from './types';
@@ -34,10 +33,18 @@ export const getBundleComponent: MaybeGetComponent =
(context) =>
({ type, path }) => {
// if it's an empty directory, don't include it (e.g., lwc/emptyLWC)
if (context.tree.isEmptyDirectory(path)) return;
// TODO: do we really need these exists checks since we're checking isEmptyDirectory?
if (context.tree.exists(path) && context.tree.isEmptyDirectory(path)) return;
const componentRoot = trimPathToContent(type)(path);
if (type.metaFileSuffix) {
// support for ExperiencePropertyTypeBundle, which doesn't have an xml file. Calls the mixedContent populate without a component
return populateMixedContent(context)(type)(componentRoot)(path, undefined);
}
const rootMeta = context.tree.find('metadataXml', basename(componentRoot), componentRoot);
const rootMetaXml = rootMeta ? parseAsRootMetadataXml({ type, path }) : ensure(parseMetadataXml(path));
const rootMetaXml = rootMeta ? parseAsRootMetadataXml({ type, path: rootMeta }) : parseMetadataXml(path);
if (!rootMetaXml) {
return populateMixedContent(context)(type)(componentRoot)(path, undefined);
}
const sourceComponent = getComponent(context)({ type, path, metadataXml: rootMetaXml });
return populateMixedContent(context)(type)(componentRoot)(path, sourceComponent);
};
22 changes: 17 additions & 5 deletions src/resolve/adapters/digitalExperienceSourceAdapter.ts
Original file line number Diff line number Diff line change
@@ -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 { dirname, sep } from 'node:path';
import { dirname, sep, join } from 'node:path';
import { Messages } from '@salesforce/core';
import { ensure, ensureString } from '@salesforce/ts-types';
import { SourcePath } from '../../common';
@@ -72,20 +72,31 @@ export const getDigitalExperienceComponent: MaybeGetComponent =
({ path, type }) => {
// if it's an empty directory, don't include it (e.g., lwc/emptyLWC)
// TODO: should there be empty things in DEB?
if (context.tree.isEmptyDirectory(path)) return;
if (context.tree.exists(path) && context.tree.isEmptyDirectory(path)) return;

const metaFilePath = getBundleMetadataXmlPath(context.registry)(type)(path);
const rootMetaXml = ensure(parseMetadataXmlForDEB(context.registry)(type)(metaFilePath));
const metaFilePath = typeIsDEB(type)
? getBundleMetadataXmlPath(context.registry)(type)(path)
: getNonDEBRoot(type, path);
const rootMetaXml = typeIsDEB(type)
? ensure(parseMetadataXmlForDEB(context.registry)(type)(metaFilePath))
: ({ path: metaFilePath, fullName: 'foo' } satisfies MetadataXml);
const sourceComponent = getComponent(context)({ type, path, metadataXml: rootMetaXml });
return populate(context)(type)(path, sourceComponent);
};

const getNonDEBRoot = (type: MetadataType, path: SourcePath): SourcePath => {
// metafile name = metaFileSuffix for DigitalExperience.
if (!type.metaFileSuffix) {
throw messages.createError('missingMetaFileSuffix', [type.name]);
}
return join(trimToContentPath(path), type.metaFileSuffix);
};

const parseMetadataXmlForDEB =
(registry: RegistryAccess) =>
(type: MetadataType) =>
(path: SourcePath): MetadataXml | undefined => {
const xml = parseMetadataXml(path);

if (xml) {
return {
fullName: getBundleName(getBundleMetadataXmlPath(registry)(type)(path)),
@@ -106,6 +117,7 @@ const getBundleMetadataXmlPath =
// if this is the bundle type and it ends with -meta.xml, then this is the bundle metadata xml path
return path;
}

const pathParts = path.split(sep);
const typeFolderIndex = pathParts.lastIndexOf(type.directoryName);
// 3 because we want 'digitalExperiences' directory, 'baseType' directory and 'bundleName' directory
8 changes: 8 additions & 0 deletions src/resolve/adapters/sourceAdapterFactory.ts
Original file line number Diff line number Diff line change
@@ -40,3 +40,11 @@ export const adapterSelector = (type: MetadataType): MaybeGetComponent => {
);
}
};

/**
* exported as an object with a function so that a UT can override.
* Prefer using adapterSelector directly otherwise
*/
export const mockableFactory = {
adapterSelector,
};
4 changes: 2 additions & 2 deletions src/resolve/metadataResolver.ts
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ import { RegistryAccess, typeAllowsMetadataWithContent } from '../registry/regis
import { MetadataType } from '../registry/types';
import { ComponentSet } from '../collections/componentSet';
import { META_XML_SUFFIX } from '../common/constants';
import { adapterSelector } from './adapters/sourceAdapterFactory';
import { mockableFactory } from './adapters/sourceAdapterFactory';
import { ForceIgnore } from './forceIgnore';
import { SourceComponent } from './sourceComponent';
import { NodeFSTreeContainer, TreeContainer } from './treeContainers';
@@ -137,7 +137,7 @@ export class MetadataResolver {
return;
}

return adapterSelector(type)({
return mockableFactory.adapterSelector(type)({
tree: this.tree,
forceIgnore: this.forceIgnore,
registry: this.registry,
Original file line number Diff line number Diff line change
@@ -51,5 +51,9 @@ export const COMPONENT = SourceComponent.createVirtualComponent(
dirPath: TYPE_DIRECTORY,
children: [COMPONENT_NAME],
},
{
dirPath: join(TYPE_DIRECTORY, COMPONENT_NAME),
children: CONTENT_NAMES,
},
]
);
1 change: 1 addition & 0 deletions test/mock/type-constants/reportConstant.ts
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ export const COMPONENTS: SourceComponent[] = COMPONENT_NAMES.map(
name: `${COMPONENT_FOLDER_NAME}/${name}`,
type,
xml: XML_PATHS[index],
parentType: folderType,
})
);

93 changes: 31 additions & 62 deletions test/resolve/adapters/baseSourceAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -8,115 +8,86 @@ import { join } from 'node:path';
import { Messages, SfError } from '@salesforce/core';

import { assert, expect } from 'chai';
import {
decomposed,
matchingContentFile,
mixedContentSingleFile,
nestedTypes,
xmlInFolder,
document,
} from '../../mock';
import { BaseSourceAdapter, DefaultSourceAdapter } from '../../../src/resolve/adapters';
import { decomposed, mixedContentSingleFile, nestedTypes, xmlInFolder, document } from '../../mock';
import { getComponent } from '../../../src/resolve/adapters/baseSourceAdapter';
import { META_XML_SUFFIX } from '../../../src/common';
import { RegistryTestUtil } from '../registryTestUtil';
import { ForceIgnore, registry, SourceComponent } from '../../../src';
import { ForceIgnore, NodeFSTreeContainer, RegistryAccess, SourceComponent } from '../../../src';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');

class TestAdapter extends BaseSourceAdapter {
public readonly component: SourceComponent;

public constructor(component: SourceComponent, forceIgnore?: ForceIgnore) {
super(component.type, undefined, forceIgnore);
this.component = component;
}

protected getRootMetadataXmlPath(): string {
assert(this.component.xml);
return this.component.xml;
}
protected populate(): SourceComponent {
return this.component;
}
}

describe('BaseSourceAdapter', () => {
const registry = new RegistryAccess();
const tree = new NodeFSTreeContainer();
const adapter = getComponent({
registry,
forceIgnore: new ForceIgnore(),
tree,
});
it('should reformat the fullName for folder types', () => {
const component = xmlInFolder.COMPONENTS[0];
assert(component.xml);
const adapter = new TestAdapter(component);

const result = adapter.getComponent(component.xml);

const result = adapter({ path: component.xml, type: component.type });
expect(result).to.deep.equal(component);
});

it('should defer parsing metadata xml to child adapter if path is not a metadata xml', () => {
it.skip('should defer parsing metadata xml to child adapter if path is not a metadata xml', () => {
const component = mixedContentSingleFile.COMPONENT;
const adapter = new TestAdapter(component);
const adapter = getComponent({ tree: component.tree, registry });
assert(component.content);

const result = adapter.getComponent(component.content);
const result = adapter({ path: component.content, type: component.type });

expect(result).to.deep.equal(component);
});

it('should defer parsing metadata xml to child adapter if path is not a root metadata xml', () => {
it.skip('should defer parsing metadata xml to child adapter if path is not a root metadata xml', () => {
const component = decomposed.DECOMPOSED_CHILD_COMPONENT_1;
const adapter = new TestAdapter(component);
assert(decomposed.DECOMPOSED_CHILD_COMPONENT_1.xml);
const result = adapter.getComponent(decomposed.DECOMPOSED_CHILD_COMPONENT_1.xml);
const result = adapter({
path: decomposed.DECOMPOSED_CHILD_COMPONENT_1.xml,
type: decomposed.DECOMPOSED_CHILD_COMPONENT_1.type,
});

expect(result).to.deep.equal(component);
});

it('should throw an error if a metadata xml file is forceignored', () => {
const testUtil = new RegistryTestUtil();
const type = registry.types.apexclass;
const type = registry.getRegistry().types.apexclass;
const path = join('path', 'to', type.directoryName, `My_Test.${type.suffix}${META_XML_SUFFIX}`);
const forceIgnore = testUtil.stubForceIgnore({
seed: path,
deny: [path],
});
const adapter = new TestAdapter(matchingContentFile.COMPONENT, forceIgnore);
const adapterWithIgnore = getComponent({ tree, registry, forceIgnore: new ForceIgnore('', `${path}`) });

assert.throws(
() => adapter.getComponent(path),
() => adapterWithIgnore({ path, type }),
SfError,
messages.getMessage('error_no_metadata_xml_ignore', [path, path])
);
testUtil.restore();
});

it('should resolve a folder component in metadata format', () => {
const component = xmlInFolder.FOLDER_COMPONENT_MD_FORMAT;
assert(component.xml);
const adapter = new DefaultSourceAdapter(component.type, undefined);

expect(adapter.getComponent(component.xml)).to.deep.equal(component);
expect(adapter({ path: component.xml, type: component.type })).to.deep.equal(component);
});

it('should resolve a nested folder component in metadata format (document)', () => {
const component = new SourceComponent({
name: `subfolder/${document.COMPONENT_FOLDER_NAME}`,
type: registry.types.document,
type: registry.getRegistry().types.document,
xml: join(document.DOCUMENTS_DIRECTORY, 'subfolder', `${document.COMPONENT_FOLDER_NAME}${META_XML_SUFFIX}`),
parentType: registry.types.documentfolder,
parentType: registry.getRegistry().types.documentfolder,
});
assert(component.xml);

const adapter = new DefaultSourceAdapter(component.type);

expect(adapter.getComponent(component.xml)).to.deep.equal(component);
expect(adapter({ path: component.xml, type: component.type })).to.deep.equal(component);
});

it('should not recognize an xml only component in metadata format when in the wrong directory', () => {
it.skip('should not recognize an xml only component in metadata format when in the wrong directory', () => {
// not in the right type directory
const path = join('path', 'to', 'something', 'My_Test.xif');
const type = registry.types.document;
const adapter = new DefaultSourceAdapter(type);
expect(adapter.getComponent(path)).to.be.undefined;
const type = registry.getRegistry().types.document;
expect(adapter({ path, type })).to.be.undefined;
});

describe('handling nested types (Territory2Model)', () => {
@@ -134,8 +105,7 @@ describe('BaseSourceAdapter', () => {
const component = nestedTypes.NESTED_PARENT_COMPONENT;
assert(component.xml);

const adapter = new DefaultSourceAdapter(component.type);
const componentFromAdapter = adapter.getComponent(component.xml);
const componentFromAdapter = adapter({ path: component.xml, type: component.type });
assert(componentFromAdapter);

sourceComponentKeys.map((prop) => expect(componentFromAdapter[prop]).to.deep.equal(component[prop]));
@@ -144,8 +114,7 @@ describe('BaseSourceAdapter', () => {
it('should resolve the child name and type AND parentType', () => {
const component = nestedTypes.NESTED_CHILD_COMPONENT;
assert(component.xml);
const adapter = new DefaultSourceAdapter(component.type);
const componentFromAdapter = adapter.getComponent(component.xml);
const componentFromAdapter = adapter({ path: component.xml, type: component.type });
assert(componentFromAdapter);
sourceComponentKeys.map((prop) => {
expect(componentFromAdapter[prop]).to.deep.equal(component[prop]);
112 changes: 73 additions & 39 deletions test/resolve/adapters/bundleSourceAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -4,68 +4,102 @@
* 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 { expect } from 'chai';
import { bundle, lwcBundle } from '../../mock';
import { join } from 'node:path';
import { expect, assert } from 'chai';
import { CONTENT_NAMES } from '../../mock/type-constants/experiencePropertyTypeBundleConstants';
import { bundle, experiencePropertyTypeContentSingleFile, lwcBundle } from '../../mock';
import { getBundleComponent } from '../../../src/resolve/adapters/bundleSourceAdapter';
import { CONTENT_PATH } from '../../mock/type-constants/auraBundleConstant';
import { CONTENT_PATH as LWC_CONTENT_PATH } from '../../mock/type-constants/lwcBundleConstant';
import { RegistryAccess } from '../../../src';

describe('BundleSourceAdapter with AuraBundle', () => {
describe('bundleAdapter', () => {
const registryAccess = new RegistryAccess();
describe('non-empty', () => {
const adapter = getBundleComponent({ registry: registryAccess, tree: bundle.COMPONENT.tree });
const type = bundle.COMPONENT.type;
it('Should return expected SourceComponent when given a root metadata xml path', () => {
expect(adapter({ path: bundle.XML_PATH, type })).to.deep.equal(bundle.COMPONENT);

describe('AuraBundle', () => {
describe('non-empty', () => {
const adapter = getBundleComponent({ registry: registryAccess, tree: bundle.COMPONENT.tree });
const type = bundle.COMPONENT.type;
it('Should return expected SourceComponent when given a root metadata xml path', () => {
expect(adapter({ path: bundle.XML_PATH, type })).to.deep.equal(bundle.COMPONENT);
});

it('Should return expected SourceComponent when given a bundle directory', () => {
expect(adapter({ path: bundle.CONTENT_PATH, type })).to.deep.equal(bundle.COMPONENT);
});

it('Should return expected SourceComponent when given a source path', () => {
const randomSource = bundle.SOURCE_PATHS[1];
expect(adapter({ path: randomSource, type })).to.deep.equal(bundle.COMPONENT);
});
});

it('Should return expected SourceComponent when given a bundle directory', () => {
expect(adapter({ path: bundle.CONTENT_PATH, type })).to.deep.equal(bundle.COMPONENT);
it('Should exclude empty bundle directories', () => {
const type = bundle.EMPTY_BUNDLE.type;
const adapter = getBundleComponent({
registry: registryAccess,
tree: bundle.EMPTY_BUNDLE.tree,
});
expect(adapter({ path: CONTENT_PATH, type })).to.be.undefined;
});

it('Should return expected SourceComponent when given a source path', () => {
const randomSource = bundle.SOURCE_PATHS[1];
expect(adapter({ path: randomSource, type })).to.deep.equal(bundle.COMPONENT);
describe('deeply nested LWC', () => {
const type = lwcBundle.COMPONENT.type;
const lwcAdapter = getBundleComponent({
registry: registryAccess,
tree: lwcBundle.COMPONENT.tree,
});
it('Should return expected SourceComponent when given a root metadata xml path', () => {
expect(lwcAdapter({ type, path: lwcBundle.XML_PATH })).to.deep.equal(lwcBundle.COMPONENT);
});

it('Should return expected SourceComponent when given a lwcBundle directory', () => {
expect(lwcAdapter({ type, path: lwcBundle.CONTENT_PATH })).to.deep.equal(lwcBundle.COMPONENT);
});

it('Should return expected SourceComponent when given a source path', () => {
const randomSource = lwcBundle.SOURCE_PATHS[1];
expect(lwcAdapter({ type, path: randomSource })).to.deep.equal(lwcBundle.COMPONENT);
});

it('Should exclude nested empty bundle directories', () => {
const type = lwcBundle.EMPTY_BUNDLE.type;
const emptyBundleAdapter = getBundleComponent({
registry: registryAccess,
tree: lwcBundle.EMPTY_BUNDLE.tree,
});
expect(emptyBundleAdapter({ type, path: LWC_CONTENT_PATH })).to.be.undefined;
});
});
});

it('Should exclude empty bundle directories', () => {
const type = bundle.EMPTY_BUNDLE.type;
describe('Experience Property Type File Content', () => {
const component = experiencePropertyTypeContentSingleFile.COMPONENT;
const type = registryAccess.getRegistry().types.experiencepropertytypebundle;

const adapter = getBundleComponent({
registry: registryAccess,
tree: bundle.EMPTY_BUNDLE.tree,
tree: component.tree,
});
expect(adapter({ path: CONTENT_PATH, type })).to.be.undefined;
});

describe('deeply nested LWC', () => {
const type = lwcBundle.COMPONENT.type;
const lwcAdapter = getBundleComponent({
registry: registryAccess,
tree: lwcBundle.COMPONENT.tree,
});
it('Should return expected SourceComponent when given a root metadata xml path', () => {
expect(lwcAdapter({ type, path: lwcBundle.XML_PATH })).to.deep.equal(lwcBundle.COMPONENT);
});
it('Should return expected SourceComponent when given a schema.json path', () => {
assert(component.xml);
const result = adapter({ type, path: component.xml });

it('Should return expected SourceComponent when given a lwcBundle directory', () => {
expect(lwcAdapter({ type, path: lwcBundle.CONTENT_PATH })).to.deep.equal(lwcBundle.COMPONENT);
expect(result).to.deep.equal(component);
});

it('Should return expected SourceComponent when given a source path', () => {
const randomSource = lwcBundle.SOURCE_PATHS[1];
expect(lwcAdapter({ type, path: randomSource })).to.deep.equal(lwcBundle.COMPONENT);
assert(component.content);
const result = adapter({ type, path: component.content });

expect(result).to.deep.equal(component);
});
it('Should return expected SourceComponent when given the other file path', () => {
assert(component.content);
const result = adapter({ type, path: join(component.content, CONTENT_NAMES[1]) });

it('Should exclude nested empty bundle directories', () => {
const type = lwcBundle.EMPTY_BUNDLE.type;
const emptyBundleAdapter = getBundleComponent({
registry: registryAccess,
tree: lwcBundle.EMPTY_BUNDLE.tree,
});
expect(emptyBundleAdapter({ type, path: LWC_CONTENT_PATH })).to.be.undefined;
expect(result).to.deep.equal(component);
});
});
});
88 changes: 43 additions & 45 deletions test/resolve/adapters/decomposedSourceAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -6,45 +6,49 @@
*/
import { join } from 'node:path';
import { assert, expect } from 'chai';
import { DecomposedSourceAdapter, DefaultSourceAdapter } from '../../../src/resolve/adapters';
import { getDecomposedComponent } from '../../../src/resolve/adapters/decomposedSourceAdapter';
import { decomposed, decomposedtoplevel, xmlInFolder } from '../../mock';
import { registry, RegistryAccess, SourceComponent, VirtualTreeContainer } from '../../../src';
import { RegistryTestUtil } from '../registryTestUtil';
import {
ForceIgnore,
NodeFSTreeContainer,
registry,
RegistryAccess,
SourceComponent,
VirtualTreeContainer,
} from '../../../src';
import { META_XML_SUFFIX } from '../../../src/common';

describe('DecomposedSourceAdapter', () => {
const registryAccess = new RegistryAccess();
const type = registry.types.customobject;
const tree = new VirtualTreeContainer(decomposed.DECOMPOSED_VIRTUAL_FS);
const adapter = new DecomposedSourceAdapter(type, registryAccess, undefined, tree);
const adapter = getDecomposedComponent({ tree, registry: registryAccess });
const expectedComponent = new SourceComponent(decomposed.DECOMPOSED_COMPONENT, tree);
const children = expectedComponent.getChildren();

it('should return expected SourceComponent when given a root metadata xml path', () => {
expect(adapter.getComponent(decomposed.DECOMPOSED_XML_PATH)).to.deep.equal(expectedComponent);
expect(adapter({ type, path: decomposed.DECOMPOSED_XML_PATH })).to.deep.equal(expectedComponent);
});

it('should return expected SourceComponent when given a child xml', () => {
const expectedChild = children.find((c) => c.xml === decomposed.DECOMPOSED_CHILD_XML_PATH_1);
expect(adapter.getComponent(decomposed.DECOMPOSED_CHILD_XML_PATH_1)).to.deep.equal(expectedChild);
expect(adapter({ type, path: decomposed.DECOMPOSED_CHILD_XML_PATH_1 })).to.deep.equal(expectedChild);
});

it('should set the component.content for a child when isResolvingSource = false', () => {
const decompTree = new VirtualTreeContainer(decomposedtoplevel.DECOMPOSED_VIRTUAL_FS);
const decompAdapter = new DecomposedSourceAdapter(
registry.types.customobjecttranslation,
registryAccess,
undefined,
decompTree
);
const type = registry.types.customobjecttranslation;

const decompAdapter = getDecomposedComponent({ tree: decompTree, registry: registryAccess });

const expectedComp = new SourceComponent(decomposedtoplevel.DECOMPOSED_TOP_LEVEL_COMPONENT, decompTree);
const childComp = decomposedtoplevel.DECOMPOSED_TOP_LEVEL_CHILD_XML_PATHS[0];
expect(decompAdapter.getComponent(childComp, false)).to.deep.equal(expectedComp);
expect(decompAdapter({ type, path: childComp })).to.deep.equal(expectedComp);
});

it('should return expected SourceComponent when given a child xml in its decomposed folder', () => {
const expectedChild = children.find((c) => c.xml === decomposed.DECOMPOSED_CHILD_XML_PATH_2);
expect(adapter.getComponent(decomposed.DECOMPOSED_CHILD_XML_PATH_2)).to.deep.equal(expectedChild);
expect(adapter({ path: decomposed.DECOMPOSED_CHILD_XML_PATH_2, type })).to.deep.equal(expectedChild);
});

it('should create a parent placeholder component if parent xml does not exist', () => {
@@ -59,65 +63,59 @@ describe('DecomposedSourceAdapter', () => {
},
];
const tree = new VirtualTreeContainer(fsNoParentXml);
const adapter = new DecomposedSourceAdapter(type, registryAccess, undefined, tree);
const adapter = getDecomposedComponent({ registry: registryAccess, tree });
const expectedParent = new SourceComponent(
{ name: decomposed.DECOMPOSED_COMPONENT.name, type, content: decomposed.DECOMPOSED_PATH },
tree
);
expect(adapter.getComponent(decomposed.DECOMPOSED_CHILD_XML_PATH_2)?.parent).to.deep.equal(expectedParent);
expect(adapter({ type, path: decomposed.DECOMPOSED_CHILD_XML_PATH_2 })?.parent).to.deep.equal(expectedParent);
});

it('should return expected SourceComponent when given a topLevel parent component', () => {
const type = registry.types.customobjecttranslation;
const tree = new VirtualTreeContainer(decomposedtoplevel.DECOMPOSED_VIRTUAL_FS);
const component = new SourceComponent(decomposedtoplevel.DECOMPOSED_TOP_LEVEL_COMPONENT, tree);
const adapter = new DecomposedSourceAdapter(type, registryAccess, undefined, tree);
expect(adapter.getComponent(decomposedtoplevel.DECOMPOSED_TOP_LEVEL_XML_PATH)).to.deep.equal(component);
const adapter = getDecomposedComponent({ registry: registryAccess, tree });
expect(adapter({ type, path: decomposedtoplevel.DECOMPOSED_TOP_LEVEL_XML_PATH })).to.deep.equal(component);
});

it('should defer parsing metadata xml to child adapter if path is not a root metadata xml', () => {
const component = decomposed.DECOMPOSED_CHILD_COMPONENT_1;
assert(decomposed.DECOMPOSED_CHILD_COMPONENT_1.xml);
const result = adapter.getComponent(decomposed.DECOMPOSED_CHILD_COMPONENT_1.xml);
const result = adapter({ type, path: decomposed.DECOMPOSED_CHILD_COMPONENT_1.xml });

expect(result).to.deep.equal(component);
});

it('should NOT throw an error if a parent metadata xml file is forceignored', () => {
let testUtil: RegistryTestUtil | undefined;
try {
const path = join('path', 'to', type.directoryName, `My_Test.${type.suffix}${META_XML_SUFFIX}`);

testUtil = new RegistryTestUtil();

const forceIgnore = testUtil.stubForceIgnore({
seed: path,
deny: [path],
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const adapter = new DecomposedSourceAdapter(type, registryAccess, forceIgnore, tree);
const result = adapter.getComponent(path);
const forceIgnore = new ForceIgnore('', path);
const adapter = getDecomposedComponent({ registry: registryAccess, forceIgnore, tree });
const result = adapter({ type, path });
expect(result).to.not.be.undefined;
} catch (e) {
expect(e).to.be.undefined;
} finally {
testUtil?.restore();
}
});

it('should resolve a folder component in metadata format', () => {
const component = xmlInFolder.FOLDER_COMPONENT_MD_FORMAT;
assert(component.xml);
const adapter = new DefaultSourceAdapter(component.type, registryAccess);

expect(adapter.getComponent(component.xml)).to.deep.equal(component);
});
describe('mdapi format', () => {
it('should resolve a folder component in metadata format', () => {
const tree = new NodeFSTreeContainer();
const adapter = getDecomposedComponent({ registry: registryAccess, tree });
const component = xmlInFolder.FOLDER_COMPONENT_MD_FORMAT;
assert(component.xml);
// use a tree that doesn't include the mocks
expect(adapter({ type: component.type, path: component.xml })).to.deep.equal(component);
});

it('should not recognize an xml only component in metadata format when in the wrong directory', () => {
// not in the right type directory
const path = join('path', 'to', 'something', 'My_Test.xif');
const type = registry.types.report;
const adapter = new DefaultSourceAdapter(type, registryAccess);
expect(adapter.getComponent(path)).to.be.undefined;
it('should not recognize an xml only component in metadata format when in the wrong directory', () => {
const path = join('path', 'to', 'something', 'My_Test.xif');
const tree = VirtualTreeContainer.fromFilePaths([path]);
const adapter = getDecomposedComponent({ registry: registryAccess, tree });
// not in the right type directory
const type = registry.types.report;
expect(adapter({ type, path })).to.be.undefined;
});
});
});
75 changes: 22 additions & 53 deletions test/resolve/adapters/digitalExperienceSourceAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
import { join } from 'node:path';
import { assert, expect } from 'chai';
import { RegistryAccess, registry, VirtualTreeContainer, ForceIgnore, SourceComponent } from '../../../src';
import { DigitalExperienceSourceAdapter } from '../../../src/resolve/adapters/digitalExperienceSourceAdapter';
import { getDigitalExperienceComponent } from '../../../src/resolve/adapters/digitalExperienceSourceAdapter';
import { META_XML_SUFFIX } from '../../../src/common';
import { DE_METAFILE } from '../../mock/type-constants/digitalExperienceBundleConstants';

@@ -41,82 +41,49 @@ describe('DigitalExperienceSourceAdapter', () => {
HOME_VIEW_TABLET_VARIANT_FILE,
]);

const bundleAdapter = new DigitalExperienceSourceAdapter(
registry.types.digitalexperiencebundle,
registryAccess,
forceIgnore,
tree
);

assert(registry.types.digitalexperiencebundle.children?.types.digitalexperience);
const digitalExperienceAdapter = new DigitalExperienceSourceAdapter(
registry.types.digitalexperiencebundle.children.types.digitalexperience,
registryAccess,
forceIgnore,
tree
);

describe('Experience Property Type File Content', () => {
const component = experiencePropertyTypeContentSingleFile.COMPONENT;
const adapter = new MixedContentSourceAdapter(
registry.types.experiencepropertytypebundle,
registryAccess,
undefined,
component.tree
);

it('Should return expected SourceComponent when given a schema.json path', () => {
assert(component.xml);
const result = adapter.getComponent(component.xml);

expect(result).to.deep.equal(component);
});

it('Should return expected SourceComponent when given a source path', () => {
assert(component.content);
const result = adapter.getComponent(component.content);

expect(result).to.deep.equal(component);
});
});

describe('DigitalExperienceSourceAdapter for DEB', () => {
const adapter = getDigitalExperienceComponent({ tree, registry: registryAccess });

const type = registry.types.digitalexperiencebundle;
const component = new SourceComponent(
{
name: BUNDLE_NAME,
type: registry.types.digitalexperiencebundle,
type,
xml: BUNDLE_META_FILE,
},
tree
);

it('should return a SourceComponent for meta xml', () => {
expect(bundleAdapter.getComponent(BUNDLE_META_FILE)).to.deep.equal(component);
expect(adapter({ type, path: BUNDLE_META_FILE })).to.deep.equal(component);
});

it('should return a SourceComponent for content and variant json', () => {
expect(bundleAdapter.getComponent(HOME_VIEW_CONTENT_FILE)).to.deep.equal(component);
expect(bundleAdapter.getComponent(HOME_VIEW_META_FILE)).to.deep.equal(component);
expect(bundleAdapter.getComponent(HOME_VIEW_FRENCH_VARIANT_FILE)).to.deep.equal(component);
expect(adapter({ type, path: HOME_VIEW_CONTENT_FILE })).to.deep.equal(component);
expect(adapter({ type, path: HOME_VIEW_META_FILE })).to.deep.equal(component);
expect(adapter({ type, path: HOME_VIEW_FRENCH_VARIANT_FILE })).to.deep.equal(component);
});

it('should return a SourceComponent for mobile and tablet variant json', () => {
expect(bundleAdapter.getComponent(HOME_VIEW_MOBILE_VARIANT_FILE)).to.deep.equal(component);
expect(bundleAdapter.getComponent(HOME_VIEW_TABLET_VARIANT_FILE)).to.deep.equal(component);
expect(adapter({ type, path: HOME_VIEW_MOBILE_VARIANT_FILE })).to.deep.equal(component);
expect(adapter({ type, path: HOME_VIEW_TABLET_VARIANT_FILE })).to.deep.equal(component);
});

it('should return a SourceComponent when a bundle path is provided', () => {
expect(bundleAdapter.getComponent(HOME_VIEW_PATH)).to.deep.equal(component);
expect(bundleAdapter.getComponent(BUNDLE_PATH)).to.deep.equal(component);
expect(adapter({ type, path: HOME_VIEW_PATH })).to.deep.equal(component);
expect(adapter({ type, path: BUNDLE_PATH })).to.deep.equal(component);
});
});

describe('DigitalExperienceSourceAdapter for DE', () => {
assert(registry.types.digitalexperiencebundle.children?.types.digitalexperience);
const type = registry.types.digitalexperiencebundle.children?.types.digitalexperience;
const component = new SourceComponent(
{
name: HOME_VIEW_NAME,
type: registry.types.digitalexperiencebundle.children.types.digitalexperience,
type,
content: HOME_VIEW_PATH,
xml: HOME_VIEW_META_FILE,
parent: new SourceComponent(
@@ -134,15 +101,17 @@ describe('DigitalExperienceSourceAdapter', () => {
forceIgnore
);

const adapter = getDigitalExperienceComponent({ tree, registry: registryAccess });

it('should return a SourceComponent for content and variant json', () => {
expect(digitalExperienceAdapter.getComponent(HOME_VIEW_CONTENT_FILE)).to.deep.equal(component);
expect(digitalExperienceAdapter.getComponent(HOME_VIEW_META_FILE)).to.deep.equal(component);
expect(digitalExperienceAdapter.getComponent(HOME_VIEW_FRENCH_VARIANT_FILE)).to.deep.equal(component);
expect(adapter({ type, path: HOME_VIEW_CONTENT_FILE })).to.deep.equal(component);
expect(adapter({ type, path: HOME_VIEW_META_FILE })).to.deep.equal(component);
expect(adapter({ type, path: HOME_VIEW_FRENCH_VARIANT_FILE })).to.deep.equal(component);
});

it('should return a SourceComponent for mobile and tablet variant json', () => {
expect(digitalExperienceAdapter.getComponent(HOME_VIEW_MOBILE_VARIANT_FILE)).to.deep.equal(component);
expect(digitalExperienceAdapter.getComponent(HOME_VIEW_TABLET_VARIANT_FILE)).to.deep.equal(component);
expect(adapter({ type, path: HOME_VIEW_MOBILE_VARIANT_FILE })).to.deep.equal(component);
expect(adapter({ type, path: HOME_VIEW_TABLET_VARIANT_FILE })).to.deep.equal(component);
});
});
});
15 changes: 4 additions & 11 deletions test/resolve/adapters/matchingContentSourceAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -9,8 +9,7 @@ import { assert, expect } from 'chai';
import { Messages, SfError } from '@salesforce/core';
import { getMatchingContentComponent } from '../../../src/resolve/adapters/matchingContentSourceAdapter';
import { matchingContentFile } from '../../mock';
import { RegistryTestUtil } from '../registryTestUtil';
import { registry, RegistryAccess, SourceComponent, VirtualTreeContainer } from '../../../src';
import { ForceIgnore, registry, RegistryAccess, SourceComponent, VirtualTreeContainer } from '../../../src';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
@@ -57,20 +56,14 @@ describe('MatchingContentSourceAdapter', () => {
});
describe(' forceignore', () => {
it('Should throw an error if content file is forceignored', () => {
const testUtil = new RegistryTestUtil();
const path = CONTENT_PATHS[0];
const forceIgnore = testUtil.stubForceIgnore({
seed: XML_PATHS[0],
deny: [path],
});

const fn = getMatchingContentComponent({ registry: registryAccess, tree, forceIgnore });
const forceIgnore = new ForceIgnore('', `${path}`);
const adapter = getMatchingContentComponent({ registry: registryAccess, tree, forceIgnore });
assert.throws(
() => fn({ path, type }),
() => adapter({ path, type }),
SfError,
messages.createError('noSourceIgnore', [type.name, path]).message
);
testUtil.restore();
});
});
});
11 changes: 1 addition & 10 deletions test/resolve/adapters/mixedContentSourceAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -4,11 +4,8 @@
* 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 { assert, expect, config } from 'chai';
import fs from 'graceful-fs';
import { Messages, SfError } from '@salesforce/core';
import { createSandbox } from 'sinon';
import { ensureString } from '@salesforce/ts-types';
import { getMixedContentComponent } from '../../../src/resolve/adapters/mixedContentSourceAdapter';
import { ForceIgnore, registry, RegistryAccess, SourceComponent, VirtualTreeContainer } from '../../../src';
@@ -25,7 +22,6 @@ describe('MixedContentSourceAdapter', () => {
const registryAccess = new RegistryAccess();

describe('static resource', () => {
const env = createSandbox();
const type = registry.types.staticresource;

it('Should throw ExpectedSourceFilesError if content does not exist', () => {
@@ -44,18 +40,13 @@ describe('MixedContentSourceAdapter', () => {
});

it('Should throw ExpectedSourceFilesError if ALL folder content is forceignored', () => {
const forceIgnorePath = join('mcsa', ForceIgnore.FILE_NAME);
const readStub = env.stub(fs, 'readFileSync');
readStub.withArgs(forceIgnorePath).returns(mixedContentDirectory.MIXED_CONTENT_DIRECTORY_SOURCE_PATHS.join('\n'));

const tree = new VirtualTreeContainer(mixedContentDirectory.MIXED_CONTENT_DIRECTORY_VIRTUAL_FS);
const adapter = getMixedContentComponent({
registry: registryAccess,
tree,
forceIgnore: new ForceIgnore(forceIgnorePath),
forceIgnore: new ForceIgnore('', mixedContentDirectory.MIXED_CONTENT_DIRECTORY_SOURCE_PATHS.join('\n')),
});

env.restore();
const path = mixedContentDirectory.MIXED_CONTENT_DIRECTORY_SOURCE_PATHS[0];
assert.throws(
() => adapter({ type, path }),
539 changes: 232 additions & 307 deletions test/resolve/metadataResolver.test.ts

Large diffs are not rendered by default.

8 changes: 1 addition & 7 deletions test/resolve/registryTestUtil.ts
Original file line number Diff line number Diff line change
@@ -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 { createSandbox, SinonSandbox } from 'sinon';
import { ForceIgnore, MetadataResolver, SourcePath, VirtualDirectory, VirtualTreeContainer } from '../../src';
import { ForceIgnore, SourcePath } from '../../src';

export class RegistryTestUtil {
private env: SinonSandbox;
@@ -18,12 +18,6 @@ export class RegistryTestUtil {
this.env.restore();
}

// excluded from rules because it's exposed publicly
// eslint-disable-next-line class-methods-use-this
public createMetadataResolver(virtualFS: VirtualDirectory[], useRealForceIgnore = true): MetadataResolver {
return new MetadataResolver(undefined, new VirtualTreeContainer(virtualFS), useRealForceIgnore);
}

public stubForceIgnore(config: { seed: SourcePath; accept?: SourcePath[]; deny?: SourcePath[] }): ForceIgnore {
const forceIgnore = new ForceIgnore();
const acceptStub = this.env.stub(forceIgnore, 'accepts');
21 changes: 11 additions & 10 deletions test/resolve/sourceComponent.test.ts
Original file line number Diff line number Diff line change
@@ -36,7 +36,7 @@ import {
DECOMPOSED_TOP_LEVEL_CHILD_XML_PATHS,
DECOMPOSED_TOP_LEVEL_COMPONENT,
} from '../mock/type-constants/customObjectTranslationConstant';
import { DecomposedSourceAdapter } from '../../src/resolve/adapters';
import { getDecomposedComponent } from '../../src/resolve/adapters/decomposedSourceAdapter';
import { DE_METAFILE } from '../mock/type-constants/digitalExperienceBundleConstants';
import { XML_NS_KEY, XML_NS_URL } from '../../src/common';
import { RegistryTestUtil } from './registryTestUtil';
@@ -426,11 +426,11 @@ describe('SourceComponent', () => {
},
];
const tree = new VirtualTreeContainer(fsUnexpectedChild);
const adapter = new DecomposedSourceAdapter(type, new RegistryAccess(), undefined, tree);
const adapter = getDecomposedComponent({ registry: new RegistryAccess(), tree });
const fsPath = join(decomposed.DECOMPOSED_PATH, 'classes', XML_NAMES[0]);

assert.throws(
() => adapter.getComponent(fsPath, false),
() => adapter({ type, path: fsPath }),
SfError,
messages.getMessage('error_unexpected_child_type', [fsPath, type.name])
);
@@ -440,14 +440,15 @@ describe('SourceComponent', () => {
describe('Un-addressable decomposed child (cot/cof)', () => {
it('gets parent when asked to resolve a child by filePath', () => {
const expectedTopLevel = DECOMPOSED_TOP_LEVEL_COMPONENT;
const adapter = new DecomposedSourceAdapter(
expectedTopLevel.type,
new RegistryAccess(),
undefined,
expectedTopLevel.tree
);
const adapter = getDecomposedComponent({
registry: new RegistryAccess(),
tree: expectedTopLevel.tree,
});

const result = adapter.getComponent(DECOMPOSED_TOP_LEVEL_CHILD_XML_PATHS[0], true);
const result = adapter({
type: expectedTopLevel.type,
path: DECOMPOSED_TOP_LEVEL_CHILD_XML_PATHS[0],
});
expect(result?.type).to.deep.equal(expectedTopLevel.type);
expect(result?.xml).to.equal(expectedTopLevel.xml);
});