Skip to content

Commit b3cfcb2

Browse files
Bryan PowellAnanyaJha
authored andcommitted
feat: support decomposed components across multiple directories (#224)
1 parent 3f1ef4b commit b3cfcb2

26 files changed

+762
-603
lines changed

src/collections/componentSet.ts

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
FromSourceOptions,
2222
FromManifestOptions,
2323
PackageManifestObject,
24-
SourceComponentOptions,
24+
ResolveOptions,
2525
} from './types';
2626
import { ComponentLike } from '../common/types';
2727

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

@@ -67,11 +67,11 @@ export class ComponentSet implements Iterable<MetadataComponent> {
6767
*/
6868
public static async fromManifestFile(
6969
fsPath: string,
70-
options?: FromManifestOptions
70+
options: FromManifestOptions = {}
7171
): Promise<ComponentSet> {
72-
const registry = options?.registry ?? new RegistryAccess();
73-
const tree = options?.tree ?? new NodeFSTreeContainer();
74-
const shouldResolve = !!options?.resolve;
72+
const registry = options.registry ?? new RegistryAccess();
73+
const tree = options.tree ?? new NodeFSTreeContainer();
74+
const shouldResolve = !!options.resolve;
7575

7676
const ws = new ComponentSet(undefined, registry);
7777
const filterSet = new ComponentSet(undefined, registry);
@@ -215,35 +215,47 @@ export class ComponentSet implements Iterable<MetadataComponent> {
215215
* @param fsPath: File path to resolve
216216
* @param options
217217
*/
218-
public resolveSourceComponents(fsPath: string, options?: SourceComponentOptions): ComponentSet {
218+
public resolveSourceComponents(fsPath: string, options: ResolveOptions = {}): ComponentSet {
219219
let filterSet: ComponentSet;
220220

221221
if (options?.filter) {
222222
const { filter } = options;
223223
filterSet = filter instanceof ComponentSet ? filter : new ComponentSet(filter);
224224
}
225225

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

231231
for (const component of resolved) {
232-
const shouldResolve = !filterSet || filterSet.has(component);
233-
const includedInWildcard = filterSet?.has({
234-
fullName: ComponentSet.WILDCARD,
235-
type: component.type,
236-
});
237-
if (shouldResolve || includedInWildcard) {
238-
this.add(component);
239-
sourceComponents.add(component);
240-
} else if (filterSet) {
241-
for (const childComponent of component.getChildren()) {
242-
if (filterSet.has(childComponent)) {
243-
this.add(childComponent);
244-
sourceComponents.add(childComponent);
232+
if (filterSet) {
233+
const includedInWildcard = filterSet.has({
234+
fullName: ComponentSet.WILDCARD,
235+
type: component.type,
236+
});
237+
const parentInFilter =
238+
component.parent &&
239+
(filterSet.has(component.parent) ||
240+
filterSet.has({
241+
fullName: ComponentSet.WILDCARD,
242+
type: component.parent.type,
243+
}));
244+
if (filterSet.has(component) || includedInWildcard || parentInFilter) {
245+
this.add(component);
246+
sourceComponents.add(component);
247+
} else {
248+
// have to check for any individually addressed children in the filter set
249+
for (const childComponent of component.getChildren()) {
250+
if (filterSet.has(childComponent)) {
251+
this.add(childComponent);
252+
sourceComponents.add(childComponent);
253+
}
245254
}
246255
}
256+
} else {
257+
this.add(component);
258+
sourceComponents.add(component);
247259
}
248260
}
249261

src/collections/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
export { ComponentSet } from './componentSet';
8-
export { FromSourceOptions, FromManifestOptions, SourceComponentOptions } from './types';
8+
export { FromSourceOptions, FromManifestOptions, ResolveOptions } from './types';

src/collections/types.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import { MetadataComponent } from '../common';
7+
import { ComponentLike } from '../common';
88
import { RegistryAccess, TreeContainer } from '../metadata-registry';
99
import { ComponentSet } from './componentSet';
1010

@@ -20,11 +20,12 @@ export interface PackageManifestObject {
2020
};
2121
}
2222

23-
export interface ComponentSetOptions {
23+
interface ComponentSetOptions {
2424
registry?: RegistryAccess;
2525
}
2626

2727
export interface FromSourceOptions extends ComponentSetOptions {
28+
filter?: Iterable<ComponentLike> | ComponentSet;
2829
tree?: TreeContainer;
2930
}
3031

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

36-
export interface SourceComponentOptions {
37-
tree?: TreeContainer;
38-
filter?: MetadataComponent[] | ComponentSet;
39-
}
37+
export type ResolveOptions = Omit<FromSourceOptions, 'registry'>;

src/convert/convertContext.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright (c) 2020, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import { WriteInfo, WriterFormat } from './types';
8+
import { SourceComponent } from '../metadata-registry';
9+
import { join } from 'path';
10+
import { JsToXml } from './streams';
11+
import { MetadataComponent } from '../common';
12+
import { JsonArray, JsonMap } from '@salesforce/ts-types';
13+
import { ComponentSet } from '../collections';
14+
15+
abstract class ConvertTransactionFinalizer<T> {
16+
protected abstract _state: T;
17+
18+
public setState(props: (state: T) => void): void {
19+
props(this._state);
20+
}
21+
22+
get state(): T {
23+
return this._state;
24+
}
25+
26+
public abstract finalize(): Promise<WriterFormat[]>;
27+
}
28+
29+
export interface RecompositionState {
30+
[componentKey: string]: {
31+
/**
32+
* Parent component that children are rolled up into
33+
*/
34+
component?: SourceComponent;
35+
/**
36+
* Children to be rolled up into the parent file
37+
*/
38+
children?: ComponentSet;
39+
};
40+
}
41+
42+
/**
43+
* Merges child components that share the same parent in the conversion pipeline
44+
* into a single file.
45+
*/
46+
class RecompositionFinalizer extends ConvertTransactionFinalizer<RecompositionState> {
47+
protected _state: RecompositionState = {};
48+
49+
public async finalize(): Promise<WriterFormat[]> {
50+
const writerData: WriterFormat[] = [];
51+
52+
for (const { component: parent, children } of Object.values(this.state)) {
53+
const baseObject: JsonMap = await parent.parseXml();
54+
const recomposedXmlObj = await this.recompose(children, baseObject);
55+
writerData.push({
56+
component: parent,
57+
writeInfos: [
58+
{
59+
source: new JsToXml(recomposedXmlObj),
60+
output: join(parent.type.directoryName, `${parent.fullName}.${parent.type.suffix}`),
61+
},
62+
],
63+
});
64+
}
65+
66+
return writerData;
67+
}
68+
69+
private async recompose(children: ComponentSet, baseXmlObj: any): Promise<JsonMap> {
70+
for (const child of children) {
71+
const { directoryName: groupNode } = child.type;
72+
const { name: parentName } = child.parent.type;
73+
const xmlObj = await (child as SourceComponent).parseXml();
74+
const childContents = xmlObj[child.type.name];
75+
76+
if (!baseXmlObj[parentName][groupNode]) {
77+
baseXmlObj[parentName][groupNode] = [];
78+
}
79+
(baseXmlObj[parentName][groupNode] as JsonArray).push(childContents);
80+
}
81+
return baseXmlObj;
82+
}
83+
}
84+
85+
export interface DecompositionState {
86+
[componentKey: string]: {
87+
foundMerge?: boolean;
88+
writeInfo?: WriteInfo;
89+
origin?: MetadataComponent;
90+
};
91+
}
92+
93+
/**
94+
* Creates write infos for any children that haven't been written yet. Children may
95+
* delay being written in order to find potential existing children to merge
96+
* with in the conversion pipeline.
97+
*/
98+
class DecompositionFinalizer extends ConvertTransactionFinalizer<DecompositionState> {
99+
protected _state: DecompositionState = {};
100+
101+
public async finalize(): Promise<WriterFormat[]> {
102+
const writerData: WriterFormat[] = [];
103+
104+
for (const toDecompose of Object.values(this._state)) {
105+
if (!toDecompose.foundMerge) {
106+
writerData.push({
107+
component: toDecompose.origin.parent ?? toDecompose.origin,
108+
writeInfos: [toDecompose.writeInfo],
109+
});
110+
}
111+
}
112+
113+
return writerData;
114+
}
115+
}
116+
117+
/**
118+
* A state manager over the course of a single metadata conversion call.
119+
*/
120+
export class ConvertContext {
121+
public readonly decomposition = new DecompositionFinalizer();
122+
public readonly recomposition = new RecompositionFinalizer();
123+
124+
public async *executeFinalizers(): AsyncIterable<WriterFormat[]> {
125+
for (const member of Object.values(this)) {
126+
if (member instanceof ConvertTransactionFinalizer) {
127+
yield member.finalize();
128+
}
129+
}
130+
}
131+
}

src/convert/convertTransaction.ts

Lines changed: 0 additions & 80 deletions
This file was deleted.

src/convert/metadataConverter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class MetadataConverter {
9090

9191
const conversionPipeline = pipeline(
9292
new ComponentReader(components),
93-
new ComponentConverter(targetFormat, this.registry, undefined, mergeSet),
93+
new ComponentConverter(targetFormat, this.registry, mergeSet),
9494
writer
9595
);
9696
tasks.push(conversionPipeline);

0 commit comments

Comments
 (0)