UNPKG

sf-decomposer

Version:

Decompose Salesforce metadata into granular, VCS-friendly files; recompose for deployment.

131 lines 7.67 kB
'use strict'; import { readFile } from 'node:fs/promises'; import { join, relative, resolve, sep } from 'node:path'; import { verifyXmlRoundtrip } from 'config-disassembler'; import { resolveDecomposeOptionsForComponent, resolveDecomposeOptionsForType, } from '../helpers/configOverrides.js'; import { CONCURRENCY_LIMITS, SFDX_PROJECT_FILE_NAME } from '../helpers/constants.js'; import { pLimit } from '../helpers/pLimit.js'; import { getRegistryValuesBySuffix } from '../metadata/getRegistryValuesBySuffix.js'; import { listParentXmlFilesForType } from '../metadata/listParentXmlFiles.js'; import { resolveEffectiveMetadataTypes } from '../metadata/parseManifest.js'; import { getRepoRoot } from '../service/core/getRepoRoot.js'; import { resolveEffectiveDisassembleOptions } from '../service/decompose/resolveEffectiveDisassembleOptions.js'; /** * Run a non-destructive round-trip check directly against the user's repo. For every parent * metadata XML file belonging to the requested (or manifest-filtered) metadata types, disassemble * + reassemble it inside an isolated temp directory (via `verifyXmlRoundtrip`, which never touches * the real file) and compare the reconstructed XML against the original. * * Unlike a real decompose/recompose run, this never writes to the user's working tree at all -- * `verifyXmlRoundtrip` owns its own temp-directory isolation per file, so there is no scratch * project to build or clean up here. * * The returned `drift` array is empty when every parent XML would survive the round trip * semantically; otherwise each entry names the offending file (relative to its package directory) * and a short reason. `reordered` is informational only -- sibling/attribute order is not * preserved by design, and Salesforce treats metadata as order-agnostic. */ export async function verifyMetadataTypes(options) { const { metadataTypes, format, ignoreDirs, strategy, decomposeNestedPerms, manifest, overrides, log } = options; const { manifestFilter, effectiveTypes } = await resolveEffectiveMetadataTypes(metadataTypes, manifest, ignoreDirs, undefined, log); if (effectiveTypes.length === 0) { log('No metadata types to verify after applying the manifest filter.'); return { metadata: [], drift: [], reordered: [] }; } const { repoRoot } = (await getRepoRoot()); // Stryker disable next-line StringLiteral: JSON.parse(Buffer) defaults to UTF-8 decoding const sfdxProjectRaw = await readFile(join(repoRoot, SFDX_PROJECT_FILE_NAME), 'utf-8'); const sfdxProject = JSON.parse(sfdxProjectRaw); const packageDirs = sfdxProject.packageDirectories.map((p) => resolve(repoRoot, p.path)); const baseOptions = { format, strategy, decomposeNestedPerms, prepurge: false, postpurge: false, }; const typeLimit = pLimit(CONCURRENCY_LIMITS.METADATA_TYPES); const typeResults = await Promise.all(effectiveTypes.map((metadataType) => typeLimit(async () => { const typeResolved = resolveDecomposeOptionsForType(metadataType, baseOptions, overrides); let metaAttributes; let ignorePath; try { ({ metaAttributes, ignorePath } = await getRegistryValuesBySuffix(metadataType, 'decompose', ignoreDirs, undefined, typeResolved.uniqueIdElements)); } catch (err) { /* istanbul ignore if -- @preserve: preserves non-manifest behavior; unreachable via known CLI types */ if (!manifestFilter) throw err; /* istanbul ignore next -- @preserve: getRegistryValuesBySuffix always throws Error instances */ const message = err instanceof Error ? err.message : String(err); log(`Skipping ${metadataType}: ${message}`); return { metadataType, processed: false, drift: [], reordered: [] }; } const manifestXmlPaths = manifestFilter?.parentXmlsBySuffix.get(metadataType); const parentFiles = await listParentXmlFilesForType(metaAttributes, manifestXmlPaths); const fileLimit = pLimit(CONCURRENCY_LIMITS.SUBDIRECTORIES); const drift = []; const reordered = []; await Promise.all(parentFiles.map((parent) => fileLimit(async () => { const resolved = resolveDecomposeOptionsForComponent(metadataType, parent.fullName, typeResolved, overrides); const effective = resolveEffectiveDisassembleOptions(metadataType, resolved); const result = await verifyXmlRoundtrip({ filePath: parent.filePath, uniqueIdElements: metaAttributes.uniqueIdElements, strategy: effective.strategy, ignorePath, fileExtension: `${metadataType}-meta.xml`, multiLevel: effective.multiLevel, splitTags: effective.splitTags, sidecarElements: effective.sidecarElements, }); // `verifyXmlRoundtrip` reports "missing in round-trip output" whenever the file // can't be disassembled at all (leaf-only XML, or XML the parser rejects outright) // -- not just when reassembly loses data. A real decompose run silently skips such // files entirely (no directory is ever created, so recompose has nothing to touch), // leaving them byte-identical throughout. Treat that specific reason as a no-op, // matching what the real pipeline actually does, rather than false-flagging every // leaf-only metadata file as drift. if (result.status === 'drift' && result.reason !== 'missing in round-trip output') { const relPath = relativeToPackageDir(parent.filePath, packageDirs, repoRoot); drift.push({ path: relPath, reason: result.reason ?? 'content drift' }); } else if (result.status === 'reordered') { const relPath = relativeToPackageDir(parent.filePath, packageDirs, repoRoot); reordered.push(relPath); } }))); return { metadataType, processed: true, drift, reordered }; }))); const metadata = typeResults.filter((r) => r.processed).map((r) => r.metadataType); const drift = typeResults.flatMap((r) => r.drift); const reordered = typeResults.flatMap((r) => r.reordered); if (drift.length === 0) { log(`Round-trip verified for ${metadata.length} metadata type(s); no drift detected.`); } else { log(`Round-trip drift detected in ${drift.length} file(s):`); for (const entry of drift) { log(` - ${entry.path}: ${entry.reason}`); } } if (reordered.length > 0) { // Informational only — semantic content matches, just sibling/attribute order changed. // Salesforce treats metadata as order-agnostic, so this is safe to commit. log(`Note: ${reordered.length} file(s) round-tripped semantically but with sibling/attribute reordering:`); for (const path of reordered) { log(` - ${path}`); } } return { metadata, drift, reordered }; } /** Path of `filePath` relative to whichever package directory contains it, forward-slashed. */ function relativeToPackageDir(filePath, packageDirs, repoRoot) { const absoluteFile = resolve(filePath); const containingDir = packageDirs .filter((dir) => absoluteFile === dir || absoluteFile.startsWith(`${dir}${sep}`)) .sort((a, b) => b.length - a.length)[0]; const base = containingDir ?? repoRoot; return relative(base, absoluteFile).split(sep).join('/'); } //# sourceMappingURL=verifyMetadataTypes.js.map