Skip to content

feat: support decomposed components across multiple directories #224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jan 14, 2021
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
58 changes: 35 additions & 23 deletions src/collections/componentSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
FromSourceOptions,
FromManifestOptions,
PackageManifestObject,
SourceComponentOptions,
ResolveOptions,
} from './types';
import { ComponentLike } from '../common/types';

Expand All @@ -46,9 +46,9 @@ export class ComponentSet implements Iterable<MetadataComponent> {
* @param fsPath Path to resolve components from
* @param options
*/
public static fromSource(fsPath: string, options?: FromSourceOptions): ComponentSet {
const ws = new ComponentSet(undefined, options?.registry);
ws.resolveSourceComponents(fsPath, { tree: options?.tree });
public static fromSource(fsPath: string, options: FromSourceOptions = {}): ComponentSet {
const ws = new ComponentSet(undefined, options.registry);
ws.resolveSourceComponents(fsPath, options);
return ws;
}

Expand All @@ -67,11 +67,11 @@ export class ComponentSet implements Iterable<MetadataComponent> {
*/
public static async fromManifestFile(
fsPath: string,
options?: FromManifestOptions
options: FromManifestOptions = {}
): Promise<ComponentSet> {
const registry = options?.registry ?? new RegistryAccess();
const tree = options?.tree ?? new NodeFSTreeContainer();
const shouldResolve = !!options?.resolve;
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);
Expand Down Expand Up @@ -215,35 +215,47 @@ export class ComponentSet implements Iterable<MetadataComponent> {
* @param fsPath: File path to resolve
* @param options
*/
public resolveSourceComponents(fsPath: string, options?: SourceComponentOptions): ComponentSet {
public resolveSourceComponents(fsPath: string, options: ResolveOptions = {}): ComponentSet {
let filterSet: ComponentSet;

if (options?.filter) {
const { filter } = options;
filterSet = filter instanceof ComponentSet ? filter : new ComponentSet(filter);
}

// TODO: move filter logic to resolver W-8023153
// TODO: move most of this logic to resolver W-8023153
const resolver = new MetadataResolver(this.registry, options?.tree);
const resolved = resolver.getComponentsFromPath(fsPath);
const sourceComponents = new ComponentSet();

for (const component of resolved) {
const shouldResolve = !filterSet || filterSet.has(component);
const includedInWildcard = filterSet?.has({
fullName: ComponentSet.WILDCARD,
type: component.type,
});
if (shouldResolve || includedInWildcard) {
this.add(component);
sourceComponents.add(component);
} else if (filterSet) {
for (const childComponent of component.getChildren()) {
if (filterSet.has(childComponent)) {
this.add(childComponent);
sourceComponents.add(childComponent);
if (filterSet) {
const includedInWildcard = filterSet.has({
fullName: ComponentSet.WILDCARD,
type: component.type,
});
const parentInFilter =
component.parent &&
(filterSet.has(component.parent) ||
filterSet.has({
fullName: ComponentSet.WILDCARD,
type: component.parent.type,
}));
if (filterSet.has(component) || includedInWildcard || parentInFilter) {
this.add(component);
sourceComponents.add(component);
} else {
// have to check for any individually addressed children in the filter set
for (const childComponent of component.getChildren()) {
if (filterSet.has(childComponent)) {
this.add(childComponent);
sourceComponents.add(childComponent);
}
}
}
} else {
this.add(component);
sourceComponents.add(component);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/collections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
export { ComponentSet } from './componentSet';
export { FromSourceOptions, FromManifestOptions, SourceComponentOptions } from './types';
export { FromSourceOptions, FromManifestOptions, ResolveOptions } from './types';
10 changes: 4 additions & 6 deletions src/collections/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* 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 { ComponentLike } from '../common';
import { RegistryAccess, TreeContainer } from '../metadata-registry';
import { ComponentSet } from './componentSet';

Expand All @@ -20,11 +20,12 @@ export interface PackageManifestObject {
};
}

export interface ComponentSetOptions {
interface ComponentSetOptions {
registry?: RegistryAccess;
}

export interface FromSourceOptions extends ComponentSetOptions {
filter?: Iterable<ComponentLike> | ComponentSet;
tree?: TreeContainer;
}

Expand All @@ -33,7 +34,4 @@ export interface FromManifestOptions extends FromSourceOptions {
literalWildcard?: boolean;
}

export interface SourceComponentOptions {
tree?: TreeContainer;
filter?: MetadataComponent[] | ComponentSet;
}
export type ResolveOptions = Omit<FromSourceOptions, 'registry'>;
131 changes: 131 additions & 0 deletions src/convert/convertContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a refactor of the ConvertTransaction concept, but performs the same job

* Copyright (c) 2020, 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 { WriteInfo, WriterFormat } from './types';
import { SourceComponent } from '../metadata-registry';
import { join } from 'path';
import { JsToXml } from './streams';
import { MetadataComponent } from '../common';
import { JsonArray, JsonMap } from '@salesforce/ts-types';
import { ComponentSet } from '../collections';

abstract class ConvertTransactionFinalizer<T> {
protected abstract _state: T;

public setState(props: (state: T) => void): void {
props(this._state);
}

get state(): T {
return this._state;
}

public abstract finalize(): Promise<WriterFormat[]>;
}

export interface RecompositionState {
[componentKey: string]: {
/**
* Parent component that children are rolled up into
*/
component?: SourceComponent;
/**
* Children to be rolled up into the parent file
*/
children?: ComponentSet;
};
}

/**
* Merges child components that share the same parent in the conversion pipeline
* into a single file.
*/
class RecompositionFinalizer extends ConvertTransactionFinalizer<RecompositionState> {
protected _state: RecompositionState = {};

public async finalize(): Promise<WriterFormat[]> {
const writerData: WriterFormat[] = [];

for (const { component: parent, children } of Object.values(this.state)) {
const baseObject: JsonMap = await parent.parseXml();
const recomposedXmlObj = await this.recompose(children, baseObject);
writerData.push({
component: parent,
writeInfos: [
{
source: new JsToXml(recomposedXmlObj),
output: join(parent.type.directoryName, `${parent.fullName}.${parent.type.suffix}`),
},
],
});
}

return writerData;
}

private async recompose(children: ComponentSet, baseXmlObj: any): Promise<JsonMap> {
for (const child of children) {
const { directoryName: groupNode } = child.type;
const { name: parentName } = child.parent.type;
const xmlObj = await (child as SourceComponent).parseXml();
const childContents = xmlObj[child.type.name];

if (!baseXmlObj[parentName][groupNode]) {
baseXmlObj[parentName][groupNode] = [];
}
(baseXmlObj[parentName][groupNode] as JsonArray).push(childContents);
}
return baseXmlObj;
}
}

export interface DecompositionState {
[componentKey: string]: {
foundMerge?: boolean;
writeInfo?: WriteInfo;
origin?: MetadataComponent;
};
}

/**
* Creates write infos for any children that haven't been written yet. Children may
* delay being written in order to find potential existing children to merge
* with in the conversion pipeline.
*/
class DecompositionFinalizer extends ConvertTransactionFinalizer<DecompositionState> {
protected _state: DecompositionState = {};

public async finalize(): Promise<WriterFormat[]> {
const writerData: WriterFormat[] = [];

for (const toDecompose of Object.values(this._state)) {
if (!toDecompose.foundMerge) {
writerData.push({
component: toDecompose.origin.parent ?? toDecompose.origin,
writeInfos: [toDecompose.writeInfo],
});
}
}

return writerData;
}
}

/**
* A state manager over the course of a single metadata conversion call.
*/
export class ConvertContext {
public readonly decomposition = new DecompositionFinalizer();
public readonly recomposition = new RecompositionFinalizer();

public async *executeFinalizers(): AsyncIterable<WriterFormat[]> {
for (const member of Object.values(this)) {
if (member instanceof ConvertTransactionFinalizer) {
yield member.finalize();
}
}
}
}
80 changes: 0 additions & 80 deletions src/convert/convertTransaction.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/convert/metadataConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class MetadataConverter {

const conversionPipeline = pipeline(
new ComponentReader(components),
new ComponentConverter(targetFormat, this.registry, undefined, mergeSet),
new ComponentConverter(targetFormat, this.registry, mergeSet),
writer
);
tasks.push(conversionPipeline);
Expand Down
Loading