Skip to content
Merged
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
32 changes: 19 additions & 13 deletions src/client/metadataApiDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
];
}
Expand Down Expand Up @@ -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<string, DeployMessage[]>): 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<string, DeployMessage[]>): 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)
Expand All @@ -498,6 +503,7 @@ const warnIfUnmatchedServerResult =
);
}
});
};
const buildFileResponses = (response: MetadataApiDeployStatus): FileResponse[] =>
ensureArray(response.details?.componentSuccesses)
.concat(ensureArray(response.details?.componentFailures))
Expand Down Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion src/collections/componentSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
});
const toParse = await this.getObject(destructiveType);
toParse.Package[XML_NS_KEY] = XML_NS_URL;

return XML_DECL.concat(builder.build(toParse));
}

Expand Down
7 changes: 5 additions & 2 deletions src/resolve/adapters/baseSourceAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 };
Expand Down
7 changes: 5 additions & 2 deletions src/resolve/metadataResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down
36 changes: 27 additions & 9 deletions src/resolve/treeContainers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Buffer> {
Expand All @@ -152,15 +156,33 @@ export class NodeFSTreeContainer extends TreeContainer {
export class ZipTreeContainer extends TreeContainer {
private zip: JSZip;
private zipKeyMap: Map<string, string> = new Map<string, string>();
private dirChildren: Map<string, string[]> = new Map<string, string[]>();

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));
}
}
}
Expand All @@ -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');
}
Expand Down
4 changes: 3 additions & 1 deletion src/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MetadataXml> {
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 };
}
Expand Down
Loading