UNPKG

sf-decomposer

Version:

Split large Salesforce metadata files into version-control-friendly pieces and rebuild deployment-ready files.

184 lines 9.21 kB
'use strict'; import { resolve, basename, join } from 'node:path'; import { readFile, readdir, stat } from 'node:fs/promises'; import { ManifestResolver, RegistryAccess } from '@salesforce/source-deploy-retrieve'; import { getRepoRoot } from '../service/core/getRepoRoot.js'; import { SFDX_PROJECT_FILE_NAME } from '../helpers/constants.js'; export async function parseManifest(manifestPath, ignoreDirs, repoRootOverride) { const { repoRoot, dxConfigFilePath } = repoRootOverride ? { repoRoot: repoRootOverride, dxConfigFilePath: join(repoRootOverride, SFDX_PROJECT_FILE_NAME) } : (await getRepoRoot()); const absManifestPath = resolve(repoRoot, manifestPath); // Stryker disable next-line StringLiteral const sfdxProjectRaw = await readFile(dxConfigFilePath, 'utf-8'); const sfdxProject = JSON.parse(sfdxProjectRaw); // Stryker disable next-line ArrayDeclaration const normalizedIgnoreDirs = (ignoreDirs ?? []).map((dir) => basename(dir)); const packageDirs = sfdxProject.packageDirectories .map((directory) => resolve(repoRoot, directory.path)) .filter((dir) => !normalizedIgnoreDirs.includes(basename(dir))); const registry = new RegistryAccess(); const resolver = new ManifestResolver(undefined, registry); const { components } = await resolver.resolve(absManifestPath); // Group declared manifest entries by their effective parent metadata type. const byParentType = new Map(); for (const component of components) { const parentType = registry.getParentType(component.type.name) ?? component.type; const parentMember = component.fullName.split('.')[0]; const isWildcard = component.fullName === '*' || parentMember === '*'; let entry = byParentType.get(parentType.name); if (!entry) { entry = { parentType, parentMembers: new Set(), wildcard: false }; byParentType.set(parentType.name, entry); } if (isWildcard) { entry.wildcard = true; } else { entry.parentMembers.add(parentMember); } } const parentXmlsBySuffix = new Map(); const orderedSuffixes = []; const groupedEntries = Array.from(byParentType.values()); const resolvedPerGroup = await Promise.all(groupedEntries.map(async ({ parentType, parentMembers, wildcard }) => { const suffix = parentType.suffix; /* istanbul ignore next -- @preserve: parent metadata types always declare a suffix in SDR's registry. Stryker disable next-line all */ if (!suffix) return undefined; const typeDirs = await findTypeDirectories(packageDirs, parentType.directoryName); // Stryker disable next-line ConditionalExpression if (typeDirs.length === 0) { const unresolvedMembers = wildcard ? [] : [...parentMembers]; return { suffix, xmlPaths: new Set(), unresolvedMembers }; } const xmlPaths = new Set(); const resolvedMembers = new Set(); // Stryker disable next-line ArrayDeclaration const resolveTasks = []; if (wildcard) { resolveTasks.push(...typeDirs.map(async (typeDir) => { const found = await listParentXmlPaths(typeDir, parentType); for (const xmlPath of found) xmlPaths.add(xmlPath); })); } for (const member of parentMembers) { resolveTasks.push(...typeDirs.map(async (typeDir) => { const xmlPath = await resolveMemberXml(typeDir, parentType, member); if (xmlPath) { xmlPaths.add(xmlPath); resolvedMembers.add(member); } })); } await Promise.all(resolveTasks); const unresolvedMembers = [...parentMembers].filter((m) => !resolvedMembers.has(m)); return { suffix, xmlPaths, unresolvedMembers }; })); const unresolvedComponents = []; for (const entry of resolvedPerGroup) { /* istanbul ignore next -- @preserve: undefined only reachable via the suffix-less branch already ignored above. Stryker disable next-line ConditionalExpression */ if (!entry) continue; const { suffix, xmlPaths, unresolvedMembers } = entry; for (const member of unresolvedMembers) { unresolvedComponents.push({ type: suffix, member }); } if (xmlPaths.size === 0) continue; /* istanbul ignore else -- @preserve: multiple parent types sharing a suffix is not produced by SDR's registry. Stryker disable next-line ConditionalExpression: */ if (!parentXmlsBySuffix.has(suffix)) { parentXmlsBySuffix.set(suffix, xmlPaths); orderedSuffixes.push(suffix); } else { const existing = parentXmlsBySuffix.get(suffix); for (const xmlPath of xmlPaths) existing.add(xmlPath); } } return { parentXmlsBySuffix, suffixes: orderedSuffixes, unresolvedComponents }; } async function findTypeDirectories(packageDirs, directoryName) { const results = await Promise.all(packageDirs.map((pkgDir) => searchRecursively(pkgDir, directoryName))); return results.flat(); } async function searchRecursively(dir, targetName) { try { const entries = await readdir(dir, { withFileTypes: true }); const directMatches = entries .filter((entry) => entry.isDirectory() && entry.name === targetName) .map((entry) => join(dir, entry.name)); const nestedPromises = entries .filter((entry) => entry.isDirectory() && entry.name !== targetName) .map((entry) => searchRecursively(join(dir, entry.name), targetName)); const nested = await Promise.all(nestedPromises); return [...directMatches, ...nested.flat()]; } catch { /* istanbul ignore next -- @preserve: Filesystem permission errors are platform-specific. Stryker disable next-line BlockStatement */ return []; } } async function resolveMemberXml(typeDir, parentType, member) { const { suffix, strictDirectoryName, folderType } = parentType; /* istanbul ignore next -- @preserve: types reaching this point always have a suffix Stryker disable next-line all: paired with the istanbul-ignore above. */ if (!suffix) return undefined; // Labels type has a single file regardless of member name. if (parentType.name === 'CustomLabels') { const labelsFile = join(typeDir, `CustomLabels.${suffix}-meta.xml`); /* istanbul ignore next -- @preserve: labels file absence implies a broken labels directory */ return (await exists(labelsFile)) ? resolve(labelsFile) : undefined; } // Stryker disable next-line ConditionalExpression, BlockStatement if (folderType) { // Folder-scoped types (e.g. Report, Dashboard, EmailTemplate, Document). // Member is of the form `<folder>/<name>`; file is `<typeDir>/<folder>/<name>.<suffix>-meta.xml`. const candidate = join(typeDir, `${member}.${suffix}-meta.xml`); return (await exists(candidate)) ? resolve(candidate) : undefined; } if (strictDirectoryName) { const candidate = join(typeDir, member, `${member}.${suffix}-meta.xml`); return (await exists(candidate)) ? resolve(candidate) : undefined; } const candidate = join(typeDir, `${member}.${suffix}-meta.xml`); return (await exists(candidate)) ? resolve(candidate) : undefined; } async function listParentXmlPaths(typeDir, parentType) { const { suffix, strictDirectoryName } = parentType; /* istanbul ignore next -- @preserve: types reaching this point always have a suffix Stryker disable next-line all: paired with the istanbul-ignore above. */ if (!suffix) return []; const metaEnding = `.${suffix}-meta.xml`; if (strictDirectoryName) { const entries = await readdir(typeDir, { withFileTypes: true }); const results = await Promise.all(entries .filter((entry) => entry.isDirectory()) .map(async (entry) => { const candidate = join(typeDir, entry.name, `${entry.name}${metaEnding}`); return (await exists(candidate)) ? resolve(candidate) : undefined; })); return results.filter((found) => found !== undefined); } // Note: folder-typed parents (Report/Dashboard/EmailTemplate/Document) are not reachable here // because manifest wildcards for those types resolve to their corresponding *Folder type, // which carries no folderType property. Specific members of folder-typed parents are resolved // via resolveMemberXml below. const entries = await readdir(typeDir, { withFileTypes: true }); return entries .filter((entry) => entry.isFile() && entry.name.endsWith(metaEnding)) .map((entry) => resolve(join(typeDir, entry.name))); } async function exists(path) { try { await stat(path); return true; } catch { // Stryker disable next-line BlockStatement return false; } } //# sourceMappingURL=parseManifest.js.map