Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 14 additions & 7 deletions src/collections/componentSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,14 +441,20 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {

const typeMap = new Map<string, Set<string>>();

[...components.entries()].map(([key, cmpMap]) => {
// Optimize: use direct iteration instead of spreading arrays
for (const [key, cmpMap] of components.entries()) {
const [typeId, fullName] = splitOnFirstDelimiter(key);
const type = this.registry.getTypeByName(typeId);

// Cache components list to avoid repeated array conversions
const componentsList = cmpMap ? Array.from(cmpMap.values()) : [];

// Add children
[...(cmpMap?.values() ?? [])]
.flatMap((c) => c.getChildren())
.map((child) => addToTypeMap({ typeMap, type: child.type, fullName: child.fullName, destructiveType }));
for (const c of componentsList) {
for (const child of c.getChildren()) {
addToTypeMap({ typeMap, type: child.type, fullName: child.fullName, destructiveType });
}
}

// logic: if this is a decomposed type not being retrieved, skip its inclusion in the manifest if the parent is "empty"
if (
Expand All @@ -458,9 +464,10 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
Object.values(type.children?.types ?? {}).some((t) => t.unaddressableWithoutParent !== true) &&
Object.values(type.children?.types ?? {}).some((t) => t.isAddressable !== false)
) {
const parentComp = [...(cmpMap?.values() ?? [])].find((c) => c.fullName === fullName);
// Reuse cached componentsList
const parentComp = componentsList.find((c) => c.fullName === fullName);
if (parentComp?.xml && !objectHasSomeRealValues(type)(parentComp.parseXmlSync())) {
return;
continue;
}
}

Expand All @@ -470,7 +477,7 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
fullName: constructFullName(this.registry, type, fullName),
destructiveType,
});
});
}

const typeMembers = Array.from(typeMap.entries())
.map(([typeName, members]) => ({ members: [...members].sort(), name: typeName }))
Expand Down
53 changes: 30 additions & 23 deletions src/resolve/metadataResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import { NodeFSTreeContainer, TreeContainer } from './treeContainers';
Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');

// Pre-compile regex patterns for performance
const CLOSE_META_SUFFIX_PATTERN = /.+\.([^.-]+)(?:-.*)?\.xml/;
const FOLDER_META_XML_PATTERN = /(.+)-meta\.xml/;

/**
* Resolver for metadata type and component objects.
*
Expand Down Expand Up @@ -88,17 +92,26 @@ export class MetadataResolver {
return components;
}

for (const fsPath of this.tree
.readDirectory(dir)
.map(fnJoin(dir))
// this method isn't truly recursive, we need to sort directories before files so we look as far down as possible
// before finding the parent and returning only it - by sorting, we make it as recursive as possible
.sort(this.sortDirsFirst)) {
// Cache directory status before sorting to avoid repeated I/O calls
const entries = this.tree.readDirectory(dir).map(fnJoin(dir));
const dirStatusCache = new Map(entries.map((p) => [p, this.tree.isDirectory(p)]));
const sortDirsFirstCached = (a: string, b: string): number => {
const aIsDir = dirStatusCache.get(a);
const bIsDir = dirStatusCache.get(b);
if (aIsDir && bIsDir) return 0;
if (aIsDir && !bIsDir) return -1;
return 1;
};

// this method isn't truly recursive, we need to sort directories before files so we look as far down as possible
// before finding the parent and returning only it - by sorting, we make it as recursive as possible
for (const fsPath of entries.sort(sortDirsFirstCached)) {
if (ignore.has(fsPath)) {
continue;
}

if (this.tree.isDirectory(fsPath)) {
// Use cached directory status to avoid redundant I/O
if (dirStatusCache.get(fsPath)) {
if (resolveDirectoryAsComponent(this.registry)(this.tree)(fsPath)) {
// Filter out empty directories to prevent deployment issues
if (this.tree.readDirectory(fsPath).length === 0) {
Expand Down Expand Up @@ -139,15 +152,6 @@ export class MetadataResolver {
return components.concat(dirQueue.flatMap((d) => this.getComponentsFromPathRecursive(d, inclusiveFilter)));
}

private sortDirsFirst = (a: string, b: string): number => {
if (this.tree.isDirectory(a) && this.tree.isDirectory(b)) {
return 0;
} else if (this.tree.isDirectory(a) && !this.tree.isDirectory(b)) {
return -1;
} else {
return 1;
}
};
private resolveComponent(fsPath: string, isResolvingSource: boolean): SourceComponent | undefined {
if (this.forceIgnore?.denies(fsPath)) {
// don't resolve the component if the path is denied
Expand Down Expand Up @@ -269,7 +273,7 @@ const getSuggestionsForUnresolvedTypes =
const metaSuffix = parsedMetaXml?.suffix;
// Finds close matches for meta suffixes
// Examples: https://regex101.com/r/vbRjwy/1
const closeMetaSuffix = new RegExp(/.+\.([^.-]+)(?:-.*)?\.xml/).exec(basename(fsPath));
const closeMetaSuffix = CLOSE_META_SUFFIX_PATTERN.exec(basename(fsPath));

let guesses;

Expand Down Expand Up @@ -314,17 +318,19 @@ const parseAsFolderMetadataXml =
(registry: RegistryAccess) =>
(fsPath: string): string | undefined => {
let folderName: string | undefined;
const match = new RegExp(/(.+)-meta\.xml/).exec(basename(fsPath));
const match = FOLDER_META_XML_PATTERN.exec(basename(fsPath));
if (match && !match[1].includes('.')) {
const parts = fsPath.split(sep);
if (parts.length > 1) {
const folderContentTypesDirs = getFolderContentTypeDirNames(registry);
// check if the path contains a folder content name as a directory
const pathWithoutFile = parts.slice(0, -1);
const pathWithoutFileSet = new Set(parts.slice(0, -1));
folderContentTypesDirs.some((dirName) => {
if (pathWithoutFile.includes(dirName)) {
if (pathWithoutFileSet.has(dirName)) {
folderName = dirName;
return true; // exit early when found
}
return false;
});
}
}
Expand Down Expand Up @@ -416,11 +422,12 @@ const resolveTypeFromStrictFolder =
(registry: RegistryAccess) =>
(fsPath: string): MetadataType | undefined => {
const pathParts = fsPath.split(sep);
const pathPartsSet = new Set(pathParts);
// first, filter out types that don't appear in the path
// then iterate using for/of to allow for early break
return registry
.getStrictFolderTypes()
.filter(pathIncludesDirName(pathParts)) // the type's directory is in the path
.filter(pathIncludesDirName(pathPartsSet)) // the type's directory is in the path
.filter(folderTypeFilter(fsPath))
.find(
(type) =>
Expand Down Expand Up @@ -470,9 +477,9 @@ const folderTypeFilter =
!type.inFolder || parentName(fsPath) !== type.directoryName;

const pathIncludesDirName =
(parts: string[]) =>
(parts: Set<string>) =>
(type: MetadataType): boolean =>
parts.includes(type.directoryName);
parts.has(type.directoryName);
/**
* Any metadata xml file (-meta.xml) is potentially a root metadata file.
*
Expand Down
5 changes: 4 additions & 1 deletion src/resolve/pseudoTypes/agentResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ const getLogger = (): Logger => {
return logger;
};

// Pre-compile regex for bot version parsing (performance optimization)
const BOT_VERSION_PATTERN = /^(.+)_(\d+)$/;

/**
* Parses a bot name to extract version filtering information.
* Supports patterns:
Expand All @@ -70,7 +73,7 @@ export function parseBotVersionFilter(botName: string): {
}

// Handle specific version pattern: BotName_<number>
const versionMatch = botName.match(/^(.+)_(\d+)$/);
const versionMatch = botName.match(BOT_VERSION_PATTERN);
if (versionMatch) {
const [, baseName, versionStr] = versionMatch;
const versionNum = parseInt(versionStr, 10);
Expand Down
22 changes: 17 additions & 5 deletions src/resolve/sourceComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,12 +332,24 @@ export class SourceComponent implements MetadataComponent {

private getDecomposedChildren(dirPath: string): SourceComponent[] {
const children: SourceComponent[] = [];
const typeChildren = this.type.children;
if (!typeChildren) {
return children;
}
const suffixMap = typeChildren.suffixes;
const typesMap = typeChildren.types;
const rootSuffix = this.type.suffix;
const rootLegacySuffix = this.type.legacySuffix;

for (const fsPath of this.walk(dirPath)) {
const childXml = parseMetadataXml(fsPath);
const fileIsRootXml = childXml?.suffix === this.type.suffix || childXml?.suffix === this.type.legacySuffix;
if (childXml && !fileIsRootXml && this.type.children && childXml.suffix) {
const childTypeId = this.type.children?.suffixes[childXml.suffix];
const childType = this.type.children.types[childTypeId];
if (!childXml?.suffix) {
continue;
}
const fileIsRootXml = childXml.suffix === rootSuffix || childXml.suffix === rootLegacySuffix;
if (!fileIsRootXml) {
const childTypeId = suffixMap[childXml.suffix];
const childType = typesMap[childTypeId];
if (!childTypeId || !childType) {
void Lifecycle.getInstance().emitWarning(
`${fsPath}: Expected a child type for ${childXml.suffix} in ${this.type.name} but none was found.`
Expand All @@ -346,7 +358,7 @@ export class SourceComponent implements MetadataComponent {
const childComponent = new SourceComponent(
{
name: childType?.suffix ? baseWithoutSuffixes(fsPath, childType) : baseName(fsPath),
type: this.type.children.types[childTypeId],
type: typesMap[childTypeId],
xml: fsPath,
parent: this,
},
Expand Down
18 changes: 7 additions & 11 deletions test/resolve/metadataResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,17 +354,13 @@ describe('MetadataResolver', () => {
},
]);
access.getComponentsFromPath(path);
// isDirectory is called a few times during recursive parsing, after debugging
// we only need to verify calls made in succession are called with dirs, and then files
expect([isDirSpy.args[3][0], isDirSpy.args[4][0]]).to.deep.equal([path, join(path, 'parent.report-meta.xml')]);
expect([isDirSpy.args[7][0], isDirSpy.args[8][0]]).to.deep.equal([
join(path, 'dir1'),
join(path, 'parent.report-meta.xml'),
]);
expect([isDirSpy.args[10][0], isDirSpy.args[11][0]]).to.deep.equal([
join(path, 'dir1'),
join(path, 'dir1', 'dir1.report-meta.xml'),
]);
// With directory status caching, isDirectory is called upfront for all entries in a directory
// Verify that isDirectory was called for all the expected paths
const calledPaths = isDirSpy.args.map((args: string[]) => args[0]);
expect(calledPaths).to.include(path);
expect(calledPaths).to.include(join(path, 'dir1'));
expect(calledPaths).to.include(join(path, 'parent.report-meta.xml'));
expect(calledPaths).to.include(join(path, 'dir1', 'dir1.report-meta.xml'));
});

it('Should determine type for folder files', () => {
Expand Down
Loading