Skip to content

Commit

Permalink
feat: update from manifest initializer (#279)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bryan Powell authored and sfsholden committed Apr 7, 2021
1 parent 6283519 commit 839494d
Show file tree
Hide file tree
Showing 7 changed files with 486 additions and 302 deletions.
110 changes: 44 additions & 66 deletions src/collections/componentSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* 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 { parse as parseXml, j2xParser } from 'fast-xml-parser';
import {
MetadataApiDeploy,
MetadataApiDeployOptions,
Expand All @@ -15,7 +14,7 @@ import { MetadataComponent, XML_DECL, XML_NS_KEY, XML_NS_URL } from '../common';
import { ComponentSetError } from '../errors';
import {
MetadataResolver,
NodeFSTreeContainer,
ManifestResolver,
RegistryAccess,
SourceComponent,
TreeContainer,
Expand All @@ -28,6 +27,7 @@ import {
} from './types';
import { ComponentLike } from '../common/types';
import { LazyCollection } from './lazyCollection';
import { j2xParser } from 'fast-xml-parser';

export type DeploySetOptions = Omit<MetadataApiDeployOptions, 'components'>;
export type RetrieveSetOptions = Omit<MetadataApiRetrieveOptions, 'components'>;
Expand Down Expand Up @@ -93,7 +93,7 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
fsPaths = input.fsPaths;
registry = input.registry ?? registry;
tree = input.tree ?? tree;
inclusiveFilter = input.inclusiveFilter;
inclusiveFilter = input.include;
} else {
fsPaths = [input];
}
Expand All @@ -110,84 +110,62 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
}

/**
* Create a set by reading a manifest file in xml format. Optionally, specify a file path
* with the `resolve` option to resolve source files for the components.
* Resolve components from a manifest file in XML format.
*
* ```
* ComponentSet.fromManifestFile('/path/to/package.xml', {
* resolve: '/path/to/force-app'
* });
* ```
* see [Sample package.xml Manifest Files](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/manifest_samples.htm)
*
* @param manifestPath Path to XML file
* @returns Promise of a ComponentSet containing manifest components
*/
public static async fromManifest(manifestPath: string): Promise<ComponentSet>;
/**
* Resolve components from a manifest file in XML format.
* Customize the resolution process using an options object. For example, resolve source-backed components
* while using the manifest file as a filter.
* process using an options object, such as resolving source-backed components
* and using the manifest file as a filter.
*
* see [Sample package.xml Manifest Files](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/manifest_samples.htm)
*
* @param fsPath Path to xml file
* @param options
* @returns Promise of a ComponentSet
* @returns Promise of a ComponentSet containing manifest components
*/
public static async fromManifestFile(
fsPath: string,
options: FromManifestOptions = {}
): Promise<ComponentSet> {
const registry = options.registry ?? new RegistryAccess();
const tree = options.tree ?? new NodeFSTreeContainer();
const shouldResolve = !!options.resolve;

const ws = new ComponentSet(undefined, registry);
const filterSet = new ComponentSet(undefined, registry);
const file = await tree.readFile(fsPath);
const manifestObj: PackageManifestObject = parseXml(file.toString(), {
stopNodes: ['version'],
});

ws.apiVersion = manifestObj.Package.version;

for (const component of ComponentSet.getComponentsFromManifestObject(manifestObj, registry)) {
if (shouldResolve) {
filterSet.add(component);
public static async fromManifest(options: FromManifestOptions): Promise<ComponentSet>;
public static async fromManifest(input: string | FromManifestOptions): Promise<ComponentSet> {
const manifestPath = typeof input === 'string' ? input : input.manifestPath;
const options = (typeof input === 'object' ? input : {}) as Partial<FromManifestOptions>;

const manifestResolver = new ManifestResolver(options.tree, options.registry);
const manifest = await manifestResolver.resolve(manifestPath);
const resolveIncludeSet = options.resolveSourcePaths
? new ComponentSet([], options.registry)
: undefined;
const result = new ComponentSet([], options.registry);
result.apiVersion = manifest.apiVersion;

for (const component of manifest.components) {
if (resolveIncludeSet) {
resolveIncludeSet.add(component);
}
const memberIsWildcard = component.fullName === ComponentSet.WILDCARD;
if (!memberIsWildcard || options?.literalWildcard || !shouldResolve) {
ws.add(component);
if (!memberIsWildcard || options.forceAddWildcards || !options.resolveSourcePaths) {
result.add(component);
}
}

if (shouldResolve) {
// if it's a string, don't iterate over the characters
const toResolve =
typeof options.resolve === 'string' ? [options.resolve] : Array.from(options.resolve);
if (options.resolveSourcePaths) {
const components = ComponentSet.fromSource({
fsPaths: toResolve,
tree,
inclusiveFilter: filterSet,
registry,
fsPaths: options.resolveSourcePaths,
tree: options.tree,
include: resolveIncludeSet,
registry: options.registry,
});
for (const component of components) {
ws.add(component);
result.add(component);
}
}

return ws;
}

private static *getComponentsFromManifestObject(
obj: PackageManifestObject,
registry: RegistryAccess
): IterableIterator<MetadataComponent> {
const { types } = obj.Package;
const typeMembers = Array.isArray(types) ? types : [types];
for (const { name: typeName, members } of typeMembers) {
const fullNames = Array.isArray(members) ? members : [members];
for (const fullName of fullNames) {
let type = registry.getTypeByName(typeName);
// if there is no / delimiter and it's a type in folders, infer folder component
if (type.folderType && !fullName.includes('/')) {
type = registry.getTypeByName(type.folderType);
}
yield {
fullName,
type,
};
}
}
return result;
}

/**
Expand Down
26 changes: 22 additions & 4 deletions src/collections/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,34 @@ export interface PackageManifestObject {

export interface FromSourceOptions extends OptionalTreeRegistryOptions {
/**
* File or directory paths to resolve components against
* File paths or directory paths to resolve components against
*/
fsPaths: string[];
/**
* Only resolve components contained in the given set
*/
inclusiveFilter?: ComponentSet;
include?: ComponentSet;
}

export interface FromManifestOptions extends OptionalTreeRegistryOptions {
resolve?: Iterable<string>;
literalWildcard?: boolean;
/**
* Path to the manifest file in XML format
*/
manifestPath: string;
/**
* Paths to resolve source-backed components. The manifest file is used to
* indicate which components to include.
*/
resolveSourcePaths?: string[];
/**
* By default, wildcard members encoutered in the manifest are added to the set
* e.g. `{ fullName: '*', type: 'ApexClass' }`. If `resolveSourcePaths` is set,
* wildcard components are not added to the final set, but are used in the filter
* when resolving source-backed components to match all components with the wildcard
* type.
*
* Use this flag to always add wildcard components to the set, regardless of the other
* conditions.
*/
forceAddWildcards?: boolean;
}
1 change: 1 addition & 0 deletions src/metadata-registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import * as data from './data/registry.json';
import { deepFreeze } from '../utils/registry';
export { MetadataResolver } from './metadataResolver';
export { ManifestResolver } from './manifestResolver';
export { RegistryAccess } from './registryAccess';
export { ManifestGenerator } from './manifestGenerator';
export {
Expand Down
77 changes: 77 additions & 0 deletions src/metadata-registry/manifestResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2021, 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 { MetadataComponent } from '../common';
import { RegistryAccess } from './registryAccess';
import { NodeFSTreeContainer } from './treeContainers';
import { TreeContainer } from './types';
import { parse as parseXml } from 'fast-xml-parser';
import { normalizeToArray } from '../utils';

export interface PackageTypeMembers {
name: string;
members: string[];
}

export interface PackageManifest {
types: PackageTypeMembers[];
version: string;
}

interface ParsedPackageTypeMembers {
name: string;
members: string | string[];
}

interface ParsedPackageManifest {
types: ParsedPackageTypeMembers | ParsedPackageTypeMembers[];
version: string;
}

export interface ResolveManifestResult {
components: MetadataComponent[];
apiVersion: string;
}

/**
* Resolve MetadataComponents from a manifest file (package.xml)
*/
export class ManifestResolver {
private tree: TreeContainer;
private registry: RegistryAccess;

constructor(tree: TreeContainer = new NodeFSTreeContainer(), registry = new RegistryAccess()) {
this.tree = tree;
this.registry = registry;
}

public async resolve(manifestPath: string): Promise<ResolveManifestResult> {
const components: MetadataComponent[] = [];

const file = await this.tree.readFile(manifestPath);

const parsedManifest: ParsedPackageManifest = parseXml(file.toString(), {
stopNodes: ['version'],
}).Package;
const packageTypeMembers = normalizeToArray(parsedManifest.types);
const apiVersion = parsedManifest.version;

for (const typeMembers of packageTypeMembers) {
const typeName = typeMembers.name;
for (const fullName of normalizeToArray(typeMembers.members)) {
let type = this.registry.getTypeByName(typeName);
// if there is no / delimiter and it's a type in folders, infer folder component
if (type.folderType && !fullName.includes('/')) {
type = this.registry.getTypeByName(type.folderType);
}
components.push({ fullName, type });
}
}

return { components, apiVersion };
}
}
Loading

0 comments on commit 839494d

Please sign in to comment.