diff --git a/src/client/metadataApiDeploy.ts b/src/client/metadataApiDeploy.ts index 8945d4157..60cbb19fd 100644 --- a/src/client/metadataApiDeploy.ts +++ b/src/client/metadataApiDeploy.ts @@ -68,12 +68,15 @@ export class DeployResult implements MetadataTransferResult { // this involves FS operations, so only perform once! if (!this.fileResponses) { this.fileResponses = [ - // removes duplicates from the file responses by parsing the object into a string, used as the key of the map ...new Map( (this.components ? buildFileResponsesFromComponentSet(this.components)(this.response) : buildFileResponses(this.response) - ).map((v) => [JSON.stringify(v), v]) + ).map((v) => { + const base = `${v.type}#${v.fullName}#${v.filePath ?? ''}#${v.state}`; + const key = 'error' in v ? `${base}#${v.error}#${String(v.lineNumber)}#${String(v.columnNumber)}` : base; + return [key, v] as const; + }) ).values(), ]; } @@ -465,24 +468,26 @@ const deleteNotFoundToFileResponses = : []; }); +const MANIFEST_FILES = new Set([ + 'package.xml', + 'destructiveChanges.xml', + 'destructiveChangesPost.xml', + 'destructiveChangesPre.xml', +]); + const warnIfUnmatchedServerResult = (fr: FileResponse[]) => - (messageMap: Map): void[] => - // keep the parents and children separated for MPD scenarios where we have a parent in one, children in another package - [...messageMap.keys()].flatMap((key) => { + (messageMap: Map): void[] => { + const frKeys = new Set(fr.map((c) => `${c.type}#${c.fullName}`)); + + return [...messageMap.keys()].flatMap((key) => { const [type, fullName] = key.split('#', 2); // UIBundleResource messages are already handled by the parent UIBundle component const consumedByWebApp = type === 'UIBundleResource' && fr.some((c) => c.type === 'UIBundle' && fullName.startsWith(`${c.fullName}/`)); - if ( - !consumedByWebApp && - !fr.find((c) => c.type === type && c.fullName === fullName) && - !['package.xml', 'destructiveChanges.xml', 'destructiveChangesPost.xml', 'destructiveChangesPre.xml'].includes( - fullName - ) - ) { + if (!consumedByWebApp && !frKeys.has(key) && !MANIFEST_FILES.has(fullName)) { const deployMessage = messageMap.get(key)!.at(0)!; // Don't warn for deleted components - not found in the component set (pre-destructiveChanges) @@ -498,6 +503,7 @@ const warnIfUnmatchedServerResult = ); } }); + }; const buildFileResponses = (response: MetadataApiDeployStatus): FileResponse[] => ensureArray(response.details?.componentSuccesses) .concat(ensureArray(response.details?.componentFailures)) @@ -525,7 +531,7 @@ const buildFileResponsesFromComponentSet = (response: MetadataApiDeployStatus): FileResponse[] => { const responseMessages = getDeployMessages(response); - const fileResponses: FileResponse[] = (cs.getSourceComponents().toArray() ?? []) + const fileResponses: FileResponse[] = [...cs.getSourceComponents()] .flatMap((deployedComponent): FileResponse[] => { // UIBundle bundles get per-file status via UIBundleResource messages if ( diff --git a/src/collections/componentSet.ts b/src/collections/componentSet.ts index 3566dd9a2..091c93815 100644 --- a/src/collections/componentSet.ts +++ b/src/collections/componentSet.ts @@ -522,7 +522,7 @@ export class ComponentSet extends LazyCollection { }); const toParse = await this.getObject(destructiveType); toParse.Package[XML_NS_KEY] = XML_NS_URL; - + return XML_DECL.concat(builder.build(toParse)); } diff --git a/src/resolve/adapters/baseSourceAdapter.ts b/src/resolve/adapters/baseSourceAdapter.ts index 65ed3ceee..c4f188181 100644 --- a/src/resolve/adapters/baseSourceAdapter.ts +++ b/src/resolve/adapters/baseSourceAdapter.ts @@ -29,6 +29,9 @@ import { RegistryAccess } from '../../registry/registryAccess'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); +const NAME_AND_SUFFIX_REGEX = /(.+)\.(.+)/; +const FOLDER_META_XML_STRICT_REGEX = /(.+)-meta\.xml$/; + export abstract class BaseSourceAdapter implements SourceAdapter { protected type: MetadataType; protected registry: RegistryAccess; @@ -191,14 +194,14 @@ const parseAsContentMetadataXml = return undefined; } - const match = new RegExp(/(.+)\.(.+)/).exec(basename(path)); + const match = NAME_AND_SUFFIX_REGEX.exec(basename(path)); if (match && type.suffix === match[2]) { return { fullName: match[1], suffix: match[2], path }; } }; const parseAsFolderMetadataXml = (fsPath: SourcePath): MetadataXml | undefined => { - const match = new RegExp(/(.+)-meta\.xml$/).exec(basename(fsPath)); + const match = FOLDER_META_XML_STRICT_REGEX.exec(basename(fsPath)); const parts = fsPath.split(sep); if (match && !match[1].includes('.') && parts.length > 1) { return { fullName: match[1], suffix: undefined, path: fsPath }; diff --git a/src/resolve/metadataResolver.ts b/src/resolve/metadataResolver.ts index 84d09accf..c6b2b7e46 100644 --- a/src/resolve/metadataResolver.ts +++ b/src/resolve/metadataResolver.ts @@ -31,6 +31,9 @@ import { NodeFSTreeContainer, TreeContainer } from './treeContainers'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); +const CLOSE_META_SUFFIX_REGEX = /.+\.([^.-]+)(?:-.*)?\.xml/; +const FOLDER_META_XML_REGEX = /(.+)-meta\.xml/; + /** * Resolver for metadata type and component objects. * @@ -262,7 +265,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_REGEX.exec(basename(fsPath)); let guesses; @@ -307,7 +310,7 @@ 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_REGEX.exec(basename(fsPath)); if (match && !match[1].includes('.')) { const parts = fsPath.split(sep); if (parts.length > 1) { diff --git a/src/resolve/treeContainers.ts b/src/resolve/treeContainers.ts index e0a8e6fba..a3c5371b4 100644 --- a/src/resolve/treeContainers.ts +++ b/src/resolve/treeContainers.ts @@ -124,9 +124,13 @@ export class NodeFSTreeContainer extends TreeContainer { } public readDirectory(fsPath: SourcePath): string[] { - const resolved = this.getUpdatedFsPath(fsPath); - if (!statSync(resolved).isDirectory()) return []; - return readdirSync(resolved); + try { + return readdirSync(this.getUpdatedFsPath(fsPath)); + } catch (e) { + const code = (e as NodeJS.ErrnoException).code; + if (code === 'ENOTDIR' || code === 'ENOENT') return []; + throw e; + } } public readFile(fsPath: SourcePath): Promise { @@ -152,15 +156,33 @@ export class NodeFSTreeContainer extends TreeContainer { export class ZipTreeContainer extends TreeContainer { private zip: JSZip; private zipKeyMap: Map = new Map(); + private dirChildren: Map = new Map(); private constructor(zip: JSZip) { super(); this.zip = zip; for (const key of Object.keys(this.zip.files)) { if (key.endsWith('/')) { - this.zipKeyMap.set(key.slice(0, -1), key); + const normalized = key.slice(0, -1); + this.zipKeyMap.set(normalized, key); + const dir = dirname(normalized); + if (dir !== normalized) { + let children = this.dirChildren.get(dir); + if (!children) { + children = []; + this.dirChildren.set(dir, children); + } + children.push(basename(normalized)); + } } else { this.zipKeyMap.set(key, key); + const dir = dirname(key); + let children = this.dirChildren.get(dir); + if (!children) { + children = []; + this.dirChildren.set(dir, children); + } + children.push(basename(key)); } } } @@ -185,12 +207,8 @@ export class ZipTreeContainer extends TreeContainer { public readDirectory(fsPath: string): string[] { const resolvedPath = this.match(fsPath); if (resolvedPath && this.ensureDirectory(resolvedPath)) { - // Remove trailing path sep if it exists. JSZip always adds them for directories but - // when comparing we call `dirname()` which does not include them. const dirPath = resolvedPath.endsWith('/') ? resolvedPath.slice(0, -1) : resolvedPath; - return Object.keys(this.zip.files) - .filter((filePath) => dirname(filePath) === dirPath) - .map((filePath) => basename(filePath)); + return this.dirChildren.get(dirPath) ?? []; } throw new SfError(messages.getMessage('error_expected_directory_path', [fsPath]), 'LibraryError'); } diff --git a/src/utils/path.ts b/src/utils/path.ts index e79c2808f..d5fee2917 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -96,8 +96,10 @@ export function trimUntil(fsPath: SourcePath, part: string, untilLast = false): * @param fsPath - File path to parse * @returns MetadataXml info or undefined */ +const METADATA_XML_REGEX = /(.+)\.(.+)-meta\.xml/; + export function parseMetadataXml(fsPath: string): Optional { - const match = new RegExp(/(.+)\.(.+)-meta\.xml/).exec(basename(fsPath)); + const match = METADATA_XML_REGEX.exec(basename(fsPath)); if (match) { return { fullName: match[1], suffix: match[2], path: fsPath }; }