sf-decomposer
Version:
Split large Salesforce metadata files into version-control-friendly pieces and rebuild deployment-ready files.
184 lines • 9.21 kB
JavaScript
;
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