Skip to content

Commit

Permalink
chore: merge main
Browse files Browse the repository at this point in the history
  • Loading branch information
iowillhoit committed Aug 1, 2024
2 parents e6d262d + d174440 commit e671e8a
Show file tree
Hide file tree
Showing 13 changed files with 1,599 additions and 2,683 deletions.
1,994 changes: 414 additions & 1,580 deletions CHANGELOG.md

Large diffs are not rendered by default.

1,269 changes: 632 additions & 637 deletions METADATA_SUPPORT.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions messages/sdr.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,16 @@ If the type is available via Metadata API but not in the registry

- Open an issue <https://github.com/forcedotcom/cli/issues>
- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>

# type_name_suggestions

Confirm the metadata type name is correct. Validate against the registry at:
<https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json>

If the type is not listed in the registry, check that it has Metadata API support via the Metadata Coverage Report:
<https://developer.salesforce.com/docs/metadata-coverage>

If the type is available via Metadata API but not in the registry

- Open an issue <https://github.com/forcedotcom/cli/issues>
- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@salesforce/source-deploy-retrieve",
"version": "12.1.8",
"version": "12.1.9",
"description": "JavaScript library to run Salesforce metadata deploys and retrieves",
"main": "lib/src/index.js",
"author": "Salesforce",
Expand All @@ -25,9 +25,9 @@
"node": ">=18.0.0"
},
"dependencies": {
"@salesforce/core": "^8.2.3",
"@salesforce/core": "^8.2.8",
"@salesforce/kit": "^3.1.6",
"@salesforce/ts-types": "^2.0.10",
"@salesforce/ts-types": "^2.0.12",
"fast-levenshtein": "^3.0.0",
"fast-xml-parser": "^4.3.6",
"got": "^11.8.6",
Expand All @@ -39,22 +39,22 @@
"proxy-agent": "^6.4.0"
},
"devDependencies": {
"@jsforce/jsforce-node": "^3.3.1",
"@salesforce/cli-plugins-testkit": "^5.3.19",
"@salesforce/dev-scripts": "^10.2.2",
"@jsforce/jsforce-node": "^3.4.0",
"@salesforce/cli-plugins-testkit": "^5.3.20",
"@salesforce/dev-scripts": "^10.2.8",
"@types/deep-equal-in-any-order": "^1.0.1",
"@types/fast-levenshtein": "^0.0.4",
"@types/graceful-fs": "^4.1.9",
"@types/mime": "2.0.3",
"@types/minimatch": "^5.1.2",
"deep-equal-in-any-order": "^1.1.19",
"deepmerge": "^4.3.1",
"eslint-plugin-sf-plugin": "^1.18.12",
"eslint-plugin-sf-plugin": "^1.20.1",
"mocha-junit-reporter": "^1.23.3",
"mocha-snap": "^5.0.0",
"ts-node": "^10.9.2",
"ts-patch": "^3.2.1",
"typescript": "^5.5.3"
"typescript": "^5.5.4"
},
"scripts": {
"build": "wireit",
Expand Down
4 changes: 1 addition & 3 deletions src/client/metadataApiDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,7 @@ export class MetadataApiDeploy extends MetadataTransfer<

const connection = await this.getConnection();

// jsforce has an <any> on this
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,no-underscore-dangle
await connection.metadata._invoke('cancelDeploy', { id: this.id });
await connection.metadata.cancelDeploy(this.id);
}

protected async pre(): Promise<AsyncResult> {
Expand Down
230 changes: 107 additions & 123 deletions src/collections/componentSetBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,137 +70,132 @@ export class ComponentSetBuilder {
* @param options: options for creating a ComponentSet
*/

// eslint-disable-next-line complexity
public static async build(options: ComponentSetOptions): Promise<ComponentSet> {
const logger = Logger.childFromRoot('componentSetBuilder');
let componentSet: ComponentSet | undefined;

const { sourcepath, manifest, metadata, packagenames, apiversion, sourceapiversion, org, projectDir } = options;
const registryAccess = new RegistryAccess(undefined, projectDir);

try {
if (sourcepath) {
logger.debug(`Building ComponentSet from sourcepath: ${sourcepath.join(', ')}`);
const fsPaths = sourcepath.map(validateAndResolvePath);
componentSet = ComponentSet.fromSource({
fsPaths,
registry: registryAccess,
});
}
const { sourcepath, manifest, metadata, packagenames, org } = options;
const registry = new RegistryAccess(undefined, options.projectDir);

// Return empty ComponentSet and use packageNames in the connection via `.retrieve` options
if (packagenames) {
logger.debug(`Building ComponentSet for packagenames: ${packagenames.toString()}`);
componentSet ??= new ComponentSet(undefined, registryAccess);
}
if (sourcepath) {
logger.debug(`Building ComponentSet from sourcepath: ${sourcepath.join(', ')}`);
const fsPaths = sourcepath.map(validateAndResolvePath);
componentSet = ComponentSet.fromSource({
fsPaths,
registry,
});
}

// Resolve manifest with source in package directories.
if (manifest) {
logger.debug(`Building ComponentSet from manifest: ${manifest.manifestPath}`);
assertFileExists(manifest.manifestPath);

logger.debug(`Searching in packageDir: ${manifest.directoryPaths.join(', ')} for matching metadata`);
componentSet = await ComponentSet.fromManifest({
manifestPath: manifest.manifestPath,
resolveSourcePaths: manifest.directoryPaths,
forceAddWildcards: true,
destructivePre: manifest.destructiveChangesPre,
destructivePost: manifest.destructiveChangesPost,
registry: registryAccess,
});
}
// Return empty ComponentSet and use packageNames in the connection via `.retrieve` options
if (packagenames) {
logger.debug(`Building ComponentSet for packagenames: ${packagenames.toString()}`);
componentSet ??= new ComponentSet(undefined, registry);
}

// Resolve metadata entries with source in package directories.
if (metadata) {
logger.debug(`Building ComponentSet from metadata: ${metadata.metadataEntries.toString()}`);
const directoryPaths = metadata.directoryPaths;
componentSet ??= new ComponentSet(undefined, registryAccess);
const componentSetFilter = new ComponentSet(undefined, registryAccess);

// Build a Set of metadata entries
metadata.metadataEntries
.map(entryToTypeAndName(registryAccess))
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry: registryAccess }))
.map(addToComponentSet(componentSet))
.map(addToComponentSet(componentSetFilter));

logger.debug(`Searching for matching metadata in directories: ${directoryPaths.join(', ')}`);

// add destructive changes if defined. Because these are deletes, all entries
// are resolved to SourceComponents
if (metadata.destructiveEntriesPre) {
metadata.destructiveEntriesPre
.map(entryToTypeAndName(registryAccess))
.map(assertNoWildcardInDestructiveEntries)
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry: registryAccess }))
.map((mdComponent) => new SourceComponent({ type: mdComponent.type, name: mdComponent.fullName }))
.map(addToComponentSet(componentSet, DestructiveChangesType.PRE));
}
if (metadata.destructiveEntriesPost) {
metadata.destructiveEntriesPost
.map(entryToTypeAndName(registryAccess))
.map(assertNoWildcardInDestructiveEntries)
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry: registryAccess }))
.map((mdComponent) => new SourceComponent({ type: mdComponent.type, name: mdComponent.fullName }))
.map(addToComponentSet(componentSet, DestructiveChangesType.POST));
}
// Resolve manifest with source in package directories.
if (manifest) {
logger.debug(`Building ComponentSet from manifest: ${manifest.manifestPath}`);
assertFileExists(manifest.manifestPath);

logger.debug(`Searching in packageDir: ${manifest.directoryPaths.join(', ')} for matching metadata`);
componentSet = await ComponentSet.fromManifest({
manifestPath: manifest.manifestPath,
resolveSourcePaths: manifest.directoryPaths,
forceAddWildcards: true,
destructivePre: manifest.destructiveChangesPre,
destructivePost: manifest.destructiveChangesPost,
registry,
});
}

const resolvedComponents = ComponentSet.fromSource({
fsPaths: directoryPaths,
include: componentSetFilter,
registry: registryAccess,
});

if (resolvedComponents.forceIgnoredPaths) {
// if useFsForceIgnore = true, then we won't be able to resolve a forceignored path,
// which we need to do to get the ignored source component
const resolver = new MetadataResolver(registryAccess, undefined, false);

for (const ignoredPath of resolvedComponents.forceIgnoredPaths ?? []) {
resolver.getComponentsFromPath(ignoredPath).map((ignored) => {
componentSet = componentSet?.filter(
(resolved) => !(resolved.fullName === ignored.name && resolved.type === ignored.type)
);
});
}
componentSet.forceIgnoredPaths = resolvedComponents.forceIgnoredPaths;
}
// Resolve metadata entries with source in package directories.
if (metadata) {
logger.debug(`Building ComponentSet from metadata: ${metadata.metadataEntries.toString()}`);
const directoryPaths = metadata.directoryPaths;
componentSet ??= new ComponentSet(undefined, registry);
const componentSetFilter = new ComponentSet(undefined, registry);

// Build a Set of metadata entries
metadata.metadataEntries
.map(entryToTypeAndName(registry))
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry }))
.map(addToComponentSet(componentSet))
.map(addToComponentSet(componentSetFilter));

logger.debug(`Searching for matching metadata in directories: ${directoryPaths.join(', ')}`);

// add destructive changes if defined. Because these are deletes, all entries
// are resolved to SourceComponents
if (metadata.destructiveEntriesPre) {
metadata.destructiveEntriesPre
.map(entryToTypeAndName(registry))
.map(assertNoWildcardInDestructiveEntries)
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry }))
.map((mdComponent) => new SourceComponent({ type: mdComponent.type, name: mdComponent.fullName }))
.map(addToComponentSet(componentSet, DestructiveChangesType.PRE));
}
if (metadata.destructiveEntriesPost) {
metadata.destructiveEntriesPost
.map(entryToTypeAndName(registry))
.map(assertNoWildcardInDestructiveEntries)
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry }))
.map((mdComponent) => new SourceComponent({ type: mdComponent.type, name: mdComponent.fullName }))
.map(addToComponentSet(componentSet, DestructiveChangesType.POST));
}

resolvedComponents.toArray().map(addToComponentSet(componentSet));
const resolvedComponents = ComponentSet.fromSource({
fsPaths: directoryPaths,
include: componentSetFilter,
registry,
});

if (resolvedComponents.forceIgnoredPaths) {
// if useFsForceIgnore = true, then we won't be able to resolve a forceignored path,
// which we need to do to get the ignored source component
const resolver = new MetadataResolver(registry, undefined, false);

for (const ignoredPath of resolvedComponents.forceIgnoredPaths ?? []) {
resolver.getComponentsFromPath(ignoredPath).map((ignored) => {
componentSet = componentSet?.filter(
(resolved) => !(resolved.fullName === ignored.name && resolved.type === ignored.type)
);
});
}
componentSet.forceIgnoredPaths = resolvedComponents.forceIgnoredPaths;
}

// Resolve metadata entries with an org connection
if (org) {
componentSet ??= new ComponentSet(undefined, registryAccess);
resolvedComponents.toArray().map(addToComponentSet(componentSet));
}

logger.debug(
`Building ComponentSet from targetUsername: ${org.username} ${
metadata ? `filtered by metadata: ${metadata.metadataEntries.toString()}` : ''
}`
);
// Resolve metadata entries with an org connection
if (org) {
componentSet ??= new ComponentSet(undefined, registry);

const mdMap = metadata
? buildMapFromComponents(metadata.metadataEntries.map(entryToTypeAndName(registryAccess)))
: (new Map() as MetadataMap);
logger.debug(
`Building ComponentSet from targetUsername: ${org.username} ${
metadata ? `filtered by metadata: ${metadata.metadataEntries.toString()}` : ''
}`
);

const fromConnection = await ComponentSet.fromConnection({
usernameOrConnection: (await StateAggregator.getInstance()).aliases.getUsername(org.username) ?? org.username,
componentFilter: getOrgComponentFilter(org, mdMap, metadata),
metadataTypes: mdMap.size ? Array.from(mdMap.keys()) : undefined,
registry: registryAccess,
});
const mdMap = metadata
? buildMapFromComponents(metadata.metadataEntries.map(entryToTypeAndName(registry)))
: (new Map() as MetadataMap);

fromConnection.toArray().map(addToComponentSet(componentSet));
}
} catch (e) {
return componentSetBuilderErrorHandler(e);
const fromConnection = await ComponentSet.fromConnection({
usernameOrConnection: (await StateAggregator.getInstance()).aliases.getUsername(org.username) ?? org.username,
componentFilter: getOrgComponentFilter(org, mdMap, metadata),
metadataTypes: mdMap.size ? Array.from(mdMap.keys()) : undefined,
registry,
});

fromConnection.toArray().map(addToComponentSet(componentSet));
}

// there should have been a componentSet created by this point.
componentSet = assertComponentSetIsNotUndefined(componentSet);
componentSet.apiVersion ??= apiversion;
componentSet.sourceApiVersion ??= sourceapiversion;
componentSet.projectDirectory = projectDir;
componentSet.apiVersion ??= options.apiversion;
componentSet.sourceApiVersion ??= options.sourceapiversion;
componentSet.projectDirectory = options.projectDir;

logComponents(logger, componentSet);
return componentSet;
Expand All @@ -214,17 +209,6 @@ const addToComponentSet =
return cmp;
};

const componentSetBuilderErrorHandler = (e: unknown): never => {
if (e instanceof Error && e.message.includes('Missing metadata type definition in registry for id')) {
// to remain generic to catch missing metadata types regardless of parameters, split on '
// example message : Missing metadata type definition in registry for id 'NonExistentType'
const issueType = e.message.split("'")[1];
throw new SfError(`The specified metadata type is unsupported: [${issueType}]`);
} else {
throw e;
}
};

const validateAndResolvePath = (filepath: string): string => path.resolve(assertFileExists(filepath));

const assertFileExists = (filepath: string): string => {
Expand Down
57 changes: 57 additions & 0 deletions src/registry/levenshtein.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { Messages } from '@salesforce/core/messages';
import * as Levenshtein from 'fast-levenshtein';
import { MetadataRegistry } from './types';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');

/** "did you mean" for Metadata type names */
export const getTypeSuggestions = (registry: MetadataRegistry, typeName: string): string[] => {
const scores = getScores(
Object.values(registry.types).map((t) => t.name),
typeName
);

const guesses = getLowestScores(scores).map((guess) => guess.registryKey);
return [
...(guesses.length
? [
`Did you mean one of the following types? [${guesses.join(',')}]`,
'', // Add a blank line for better readability
]
: []),
messages.getMessage('type_name_suggestions'),
];
};

export const getSuffixGuesses = (suffixes: string[], input: string): string[] => {
const scores = getScores(suffixes, input);
return getLowestScores(scores).map((g) => g.registryKey);
};

type LevenshteinScore = {
registryKey: string;
score: number;
};

const getScores = (choices: string[], input: string): LevenshteinScore[] =>
choices.map((registryKey) => ({
registryKey,
score: Levenshtein.get(input, registryKey, { useCollator: true }),
}));

/** Levenshtein uses positive integers for scores, find all scores that match the lowest score */
const getLowestScores = (scores: LevenshteinScore[]): LevenshteinScore[] => {
const sortedScores = scores.sort(levenshteinSorter);
const lowestScore = scores[0].score;
return sortedScores.filter((score) => score.score === lowestScore);
};

const levenshteinSorter = (a: LevenshteinScore, b: LevenshteinScore): number => a.score - b.score;
Loading

2 comments on commit e671e8a

@svc-cli-bot
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: e671e8a Previous: 40f11b2 Ratio
eda-componentSetCreate-linux 185 ms 244 ms 0.76
eda-sourceToMdapi-linux 2367 ms 2432 ms 0.97
eda-sourceToZip-linux 1864 ms 1864 ms 1
eda-mdapiToSource-linux 2970 ms 2878 ms 1.03
lotsOfClasses-componentSetCreate-linux 377 ms 420 ms 0.90
lotsOfClasses-sourceToMdapi-linux 3727 ms 3701 ms 1.01
lotsOfClasses-sourceToZip-linux 3183 ms 3291 ms 0.97
lotsOfClasses-mdapiToSource-linux 3572 ms 3636 ms 0.98
lotsOfClassesOneDir-componentSetCreate-linux 645 ms 766 ms 0.84
lotsOfClassesOneDir-sourceToMdapi-linux 6517 ms 6707 ms 0.97
lotsOfClassesOneDir-sourceToZip-linux 5683 ms 5956 ms 0.95
lotsOfClassesOneDir-mdapiToSource-linux 6521 ms 6498 ms 1.00

This comment was automatically generated by workflow using github-action-benchmark.

@svc-cli-bot
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: e671e8a Previous: 40f11b2 Ratio
eda-componentSetCreate-win32 428 ms 645 ms 0.66
eda-sourceToMdapi-win32 4275 ms 4279 ms 1.00
eda-sourceToZip-win32 2931 ms 3104 ms 0.94
eda-mdapiToSource-win32 5728 ms 5919 ms 0.97
lotsOfClasses-componentSetCreate-win32 877 ms 1237 ms 0.71
lotsOfClasses-sourceToMdapi-win32 7339 ms 8018 ms 0.92
lotsOfClasses-sourceToZip-win32 4753 ms 5318 ms 0.89
lotsOfClasses-mdapiToSource-win32 7568 ms 8377 ms 0.90
lotsOfClassesOneDir-componentSetCreate-win32 1482 ms 2194 ms 0.68
lotsOfClassesOneDir-sourceToMdapi-win32 13470 ms 14751 ms 0.91
lotsOfClassesOneDir-sourceToZip-win32 8615 ms 9756 ms 0.88
lotsOfClassesOneDir-mdapiToSource-win32 13679 ms 14662 ms 0.93

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.