@uipath/maestro-builder-sdk 6.58.0 → 6.58.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/edit-scope.d.ts +180 -0
- package/dist/edit-scope.js +251 -0
- package/dist/merge-cli.js +4 -2
- package/dist/merge-compile.d.ts +5 -88
- package/dist/merge-compile.js +8 -138
- package/dist/merge.d.ts +1 -61
- package/dist/merge.js +1 -112
- package/package.json +2 -2
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edit-scope — which parts of a Flow the author edited, and where a finding
|
|
3
|
+
* sits relative to that edit.
|
|
4
|
+
*
|
|
5
|
+
* A node or subflow of the edited flow that differs from its `baseline`
|
|
6
|
+
* counterpart (compile(decompile(original)), the no-edit round trip) is one
|
|
7
|
+
* the author changed; one that equals it is untouched. `mergeFlow` takes the
|
|
8
|
+
* changed ones from the edited flow and restores the untouched ones from the
|
|
9
|
+
* original; `flow merge --baseline-from` (merge-cli.ts) sorts each draft
|
|
10
|
+
* refusal and `check()` finding by the same rule, so a finding on an untouched
|
|
11
|
+
* node is a warning and one on an edited node is an error.
|
|
12
|
+
*
|
|
13
|
+
* For instance, a canvas Slack node with no connection yet that the author did
|
|
14
|
+
* not touch is `kept`; the same node with a new message text is `refused`.
|
|
15
|
+
*
|
|
16
|
+
* Pure: no file access, so a portable entry (`check.ts`, `merge.ts`) can use
|
|
17
|
+
* it. Reading the original and compiling its baseline is merge-compile.ts's.
|
|
18
|
+
*/
|
|
19
|
+
import type { Diagnostic } from './check.js';
|
|
20
|
+
export interface AnyNode {
|
|
21
|
+
id: string;
|
|
22
|
+
type: string;
|
|
23
|
+
typeVersion?: string;
|
|
24
|
+
parentId?: string;
|
|
25
|
+
ui?: unknown;
|
|
26
|
+
display?: unknown;
|
|
27
|
+
inputs?: Record<string, unknown>;
|
|
28
|
+
outputs?: Record<string, unknown>;
|
|
29
|
+
[k: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface AnyEdge {
|
|
32
|
+
id?: string;
|
|
33
|
+
sourceNodeId: string;
|
|
34
|
+
sourcePort: string;
|
|
35
|
+
targetNodeId: string;
|
|
36
|
+
targetPort: string;
|
|
37
|
+
[k: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
export interface AnyFlow {
|
|
40
|
+
id?: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
version?: string;
|
|
43
|
+
nodes?: AnyNode[];
|
|
44
|
+
edges?: AnyEdge[];
|
|
45
|
+
definitions?: Array<{
|
|
46
|
+
nodeType?: string;
|
|
47
|
+
[k: string]: unknown;
|
|
48
|
+
}>;
|
|
49
|
+
variables?: unknown;
|
|
50
|
+
subflows?: Record<string, unknown>;
|
|
51
|
+
bindings?: Array<{
|
|
52
|
+
id?: string;
|
|
53
|
+
[k: string]: unknown;
|
|
54
|
+
}>;
|
|
55
|
+
metadata?: unknown;
|
|
56
|
+
solutionId?: unknown;
|
|
57
|
+
projectId?: unknown;
|
|
58
|
+
layout?: unknown;
|
|
59
|
+
runtime?: unknown;
|
|
60
|
+
[k: string]: unknown;
|
|
61
|
+
}
|
|
62
|
+
export declare function byId(nodes: AnyNode[] | undefined): Map<string, AnyNode>;
|
|
63
|
+
/**
|
|
64
|
+
* Whether two nodes are the SAME as far as the author is concerned — same type,
|
|
65
|
+
* same inputs and same output mappings. Layout (`ui`), `display`, `typeVersion`,
|
|
66
|
+
* and the derived parts of `outputs` (type, schema, description) are ignored,
|
|
67
|
+
* because those are exactly the parts decompile does not carry through. An
|
|
68
|
+
* output's `source` is authored, not derived: it is what an End returns, so an
|
|
69
|
+
* edit to `.return({ ... })` changes nothing else on the node. Used only to
|
|
70
|
+
* decide "did the user edit this node?" against the baseline.
|
|
71
|
+
*/
|
|
72
|
+
export declare function nodeUnchanged(a: AnyNode | undefined, b: AnyNode | undefined): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The keys of the subflows `mergeFlow` (merge.ts) takes from `edited` rather than
|
|
75
|
+
* restoring from `original`: every subflow that is new, that the original does
|
|
76
|
+
* not have, or that differs from its `baseline` counterpart in any way the
|
|
77
|
+
* author can edit — a node's inputs, output sources or label, an edge, the
|
|
78
|
+
* scope's `variables` or `layout` (`subflowUnchanged`, #1322). With no
|
|
79
|
+
* baseline, that is every subflow. A kept subflow is the original's whole: its
|
|
80
|
+
* nodes, edges, variables and layout.
|
|
81
|
+
*/
|
|
82
|
+
export declare function subflowsTakenFromEdited(original: AnyFlow, edited: AnyFlow, baseline?: AnyFlow): string[];
|
|
83
|
+
export type SubflowScope = {
|
|
84
|
+
nodes?: AnyNode[];
|
|
85
|
+
edges?: AnyEdge[];
|
|
86
|
+
variables?: unknown;
|
|
87
|
+
layout?: unknown;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Whether merge restores `e`'s ORIGINAL node instead of taking `e`: there is a
|
|
91
|
+
* baseline, the original has the node, and `e` equals its baseline counterpart.
|
|
92
|
+
* merge.ts's `mergeFlow` node loop and {@link nodesTakenFromEdited} both ask this.
|
|
93
|
+
*/
|
|
94
|
+
export declare function isKept(e: AnyNode, origNodes: Map<string, AnyNode>, baseNodes: Map<string, AnyNode>, baseline: AnyFlow | undefined): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Why a finding is an error: `edited`, its node is one the author changed or
|
|
97
|
+
* added; `subflow`, it sits in a subflow the edit changed, which merge takes
|
|
98
|
+
* whole; `unplaced`, it names no node and the original's own round trip does
|
|
99
|
+
* not report it, so the edit introduced it.
|
|
100
|
+
*/
|
|
101
|
+
export type TakenReason = 'edited' | 'subflow' | 'unplaced';
|
|
102
|
+
/**
|
|
103
|
+
* A finding on a node merge takes from the edited flow, with the reason it
|
|
104
|
+
* takes it: `edited`, the author changed or added the node; `subflow`, the
|
|
105
|
+
* node sits in a subflow the edit changed, which merge takes whole (`subflow`
|
|
106
|
+
* names it).
|
|
107
|
+
*/
|
|
108
|
+
export type Refused<T> = T & {
|
|
109
|
+
reason: TakenReason;
|
|
110
|
+
subflow?: string;
|
|
111
|
+
};
|
|
112
|
+
/** A finding on a node merge restores from the original; `subflow` names the untouched subflow it sits in. */
|
|
113
|
+
type Kept<T> = T & {
|
|
114
|
+
subflow?: string;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Sort each finding by what merge does with its node: `refused` when the node
|
|
118
|
+
* comes from the edited flow (reason `edited`: the author changed or added it;
|
|
119
|
+
* reason `subflow`: it sits in a subflow the edit changed, which merge takes
|
|
120
|
+
* whole), `kept` when merge restores the original node — a root node the edit
|
|
121
|
+
* did not touch, or any node of a subflow the edit did not touch (#1322) — and
|
|
122
|
+
* `unplaced` when the finding names no node merge knows (an unnamed End's
|
|
123
|
+
* `.return`, a flow-level finding). The caller sorts those again with
|
|
124
|
+
* {@link splitUnplaced}: one the original's own round trip reports too is
|
|
125
|
+
* printed, one the edit introduced is refused.
|
|
126
|
+
*
|
|
127
|
+
* For instance a Slack node with no connection that the author did not touch
|
|
128
|
+
* is `kept`; the same node with a new message text is `refused` as `edited`.
|
|
129
|
+
*/
|
|
130
|
+
export declare function classifyDrafts<T extends {
|
|
131
|
+
nodeId: string;
|
|
132
|
+
subflow?: string;
|
|
133
|
+
}>(original: any, edited: any, baseline: any, findings: readonly T[]): {
|
|
134
|
+
kept: Kept<T>[];
|
|
135
|
+
refused: Refused<T>[];
|
|
136
|
+
unplaced: T[];
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* A `check()` diagnostic, placed on the node its step emitted (`''` when it
|
|
140
|
+
* names no node); `subflow` is the calling step when the finding is inside a
|
|
141
|
+
* subflow, which then decides how it is classified.
|
|
142
|
+
*/
|
|
143
|
+
export type PlacedDiagnostic = Diagnostic & {
|
|
144
|
+
nodeId: string;
|
|
145
|
+
subflow?: string;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Place each `check()` diagnostic on the node its step emitted in `editedFlow`, so
|
|
149
|
+
* {@link classifyDrafts} can sort it: a root step on its node (by label, then
|
|
150
|
+
* id); a step inside a subflow (`check` reports it as `<caller> › <step>`, one
|
|
151
|
+
* segment per level) on the deepest step the path resolves to, with `subflow`
|
|
152
|
+
* set to the deepest subflow on that path, which is what decides; a named End
|
|
153
|
+
* on its End node (#1322). A diagnostic with no step, or one no node answers
|
|
154
|
+
* to, gets nodeId `''`.
|
|
155
|
+
*/
|
|
156
|
+
export declare function placeDiagnostics(diagnostics: readonly Diagnostic[], editedFlow: any): PlacedDiagnostic[];
|
|
157
|
+
/** How many times each {@link findingKey} occurs, for {@link splitUnplaced}'s baseline. */
|
|
158
|
+
export declare function findingCounts(findings: ReadonlyArray<{
|
|
159
|
+
code: string;
|
|
160
|
+
message: string;
|
|
161
|
+
}>): Map<string, number>;
|
|
162
|
+
/**
|
|
163
|
+
* Sort findings merge could not place on a node by whether the edit introduced
|
|
164
|
+
* them: `known` when the ORIGINAL's own round trip (`baseline`, the decompiled
|
|
165
|
+
* original compiled and checked, as {@link findingCounts}) reports the same
|
|
166
|
+
* code and message — once per occurrence, so a second copy of a finding the
|
|
167
|
+
* original has once is `introduced` — so the finding predates the edit and
|
|
168
|
+
* only prints; `introduced` otherwise, which is refused like any error on an
|
|
169
|
+
* edited node (#1322). With no baseline findings to compare against (a
|
|
170
|
+
* precompiled `--baseline` file), every one is `introduced`, as merge treated
|
|
171
|
+
* them before.
|
|
172
|
+
*/
|
|
173
|
+
export declare function splitUnplaced<T extends {
|
|
174
|
+
code: string;
|
|
175
|
+
message: string;
|
|
176
|
+
}>(unplaced: readonly T[], baseline: ReadonlyMap<string, number> | undefined): {
|
|
177
|
+
known: T[];
|
|
178
|
+
introduced: T[];
|
|
179
|
+
};
|
|
180
|
+
export {};
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
export function byId(nodes) {
|
|
2
|
+
return new Map((nodes ?? []).map((n) => [n.id, n]));
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Whether two nodes are the SAME as far as the author is concerned — same type,
|
|
6
|
+
* same inputs and same output mappings. Layout (`ui`), `display`, `typeVersion`,
|
|
7
|
+
* and the derived parts of `outputs` (type, schema, description) are ignored,
|
|
8
|
+
* because those are exactly the parts decompile does not carry through. An
|
|
9
|
+
* output's `source` is authored, not derived: it is what an End returns, so an
|
|
10
|
+
* edit to `.return({ ... })` changes nothing else on the node. Used only to
|
|
11
|
+
* decide "did the user edit this node?" against the baseline.
|
|
12
|
+
*/
|
|
13
|
+
export function nodeUnchanged(a, b) {
|
|
14
|
+
if (!a || !b)
|
|
15
|
+
return false;
|
|
16
|
+
if (a.type !== b.type)
|
|
17
|
+
return false;
|
|
18
|
+
return JSON.stringify(a.inputs ?? {}) === JSON.stringify(b.inputs ?? {})
|
|
19
|
+
&& JSON.stringify(outputSources(a)) === JSON.stringify(outputSources(b));
|
|
20
|
+
}
|
|
21
|
+
/** Each output's `source` by output name; outputs without one are left out. */
|
|
22
|
+
function outputSources(n) {
|
|
23
|
+
const sources = {};
|
|
24
|
+
for (const [name, output] of Object.entries(n.outputs ?? {})) {
|
|
25
|
+
if (output && typeof output === 'object' && 'source' in output)
|
|
26
|
+
sources[name] = output.source;
|
|
27
|
+
}
|
|
28
|
+
return sources;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The ids of the root nodes `mergeFlow` (merge.ts) takes from `edited` rather than
|
|
32
|
+
* restoring from `original`: every node that is new, or that differs from its
|
|
33
|
+
* `baseline` counterpart. With no baseline, that is every node.
|
|
34
|
+
*
|
|
35
|
+
* For instance, with a baseline equal to `edited` and every node present in
|
|
36
|
+
* `original`, the answer is `[]` — a no-edit merge keeps them all.
|
|
37
|
+
*/
|
|
38
|
+
function nodesTakenFromEdited(original, edited, baseline) {
|
|
39
|
+
const origNodes = byId(original.nodes);
|
|
40
|
+
const baseNodes = byId(baseline?.nodes);
|
|
41
|
+
return (edited.nodes ?? []).filter((e) => !isKept(e, origNodes, baseNodes, baseline)).map((e) => e.id);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The keys of the subflows `mergeFlow` (merge.ts) takes from `edited` rather than
|
|
45
|
+
* restoring from `original`: every subflow that is new, that the original does
|
|
46
|
+
* not have, or that differs from its `baseline` counterpart in any way the
|
|
47
|
+
* author can edit — a node's inputs, output sources or label, an edge, the
|
|
48
|
+
* scope's `variables` or `layout` (`subflowUnchanged`, #1322). With no
|
|
49
|
+
* baseline, that is every subflow. A kept subflow is the original's whole: its
|
|
50
|
+
* nodes, edges, variables and layout.
|
|
51
|
+
*/
|
|
52
|
+
export function subflowsTakenFromEdited(original, edited, baseline) {
|
|
53
|
+
return Object.keys(edited.subflows ?? {}).filter((key) => {
|
|
54
|
+
const orig = original.subflows?.[key];
|
|
55
|
+
const base = baseline?.subflows?.[key];
|
|
56
|
+
return !baseline || orig === undefined || base === undefined
|
|
57
|
+
|| !subflowUnchanged(edited.subflows[key], base);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** A node's canvas label, when it has one. */
|
|
61
|
+
const labelOf = (n) => n?.display?.label;
|
|
62
|
+
/**
|
|
63
|
+
* Whether an edited subflow equals its baseline: the same node ids, each node
|
|
64
|
+
* unchanged (`nodeUnchanged`) and under the same label, the same edges by
|
|
65
|
+
* their endpoints and handles (ids are compile's own), and the same
|
|
66
|
+
* `variables` and `layout` byte for byte. The scope's variables carry what the
|
|
67
|
+
* nodes do not — a step's `{ updates }` row, a child `.input()` type, a `.var()`
|
|
68
|
+
* default — and a kept subflow is the original's whole, so any of them changing
|
|
69
|
+
* has to make the subflow a taken one.
|
|
70
|
+
*/
|
|
71
|
+
function subflowUnchanged(e, b) {
|
|
72
|
+
const eNodes = byId(e.nodes);
|
|
73
|
+
const bNodes = byId(b.nodes);
|
|
74
|
+
if (eNodes.size !== bNodes.size)
|
|
75
|
+
return false;
|
|
76
|
+
for (const [id, n] of eNodes) {
|
|
77
|
+
const base = bNodes.get(id);
|
|
78
|
+
if (!nodeUnchanged(n, base) || labelOf(n) !== labelOf(base))
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (JSON.stringify(e.variables ?? null) !== JSON.stringify(b.variables ?? null))
|
|
82
|
+
return false;
|
|
83
|
+
// `layout.edges` is keyed by edge id, and ids come from one file-wide
|
|
84
|
+
// counter: a step added before the call shifts every id in the subflow
|
|
85
|
+
// without touching it. So routes are compared by the endpoints the id names
|
|
86
|
+
// (as the edges themselves are), and `layout.nodes` as it is.
|
|
87
|
+
if (JSON.stringify(layoutByEndpoints(e)) !== JSON.stringify(layoutByEndpoints(b)))
|
|
88
|
+
return false;
|
|
89
|
+
const eEdges = (e.edges ?? []).map(edgeKey).sort();
|
|
90
|
+
const bEdges = (b.edges ?? []).map(edgeKey).sort();
|
|
91
|
+
return eEdges.length === bEdges.length && eEdges.every((k, i) => k === bEdges[i]);
|
|
92
|
+
}
|
|
93
|
+
/** `<source>@<port>-><target>@<port>`: an edge's identity apart from its id. */
|
|
94
|
+
const edgeKey = (x) => `${x.sourceNodeId}@${x.sourcePort}->${x.targetNodeId}@${x.targetPort}`;
|
|
95
|
+
/** A scope's layout with its `edges` re-keyed by endpoints, so an id shift does not read as a change. */
|
|
96
|
+
function layoutByEndpoints(sf) {
|
|
97
|
+
const layout = sf.layout;
|
|
98
|
+
if (!layout || typeof layout !== 'object')
|
|
99
|
+
return layout ?? null;
|
|
100
|
+
const byId = new Map((sf.edges ?? []).map((x) => [x.id, edgeKey(x)]));
|
|
101
|
+
const { edges, ...rest } = layout;
|
|
102
|
+
const routes = Object.entries(edges ?? {}).map(([id, route]) => [byId.get(id) ?? id, route]).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
103
|
+
return { ...rest, edges: Object.fromEntries(routes) };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Whether merge restores `e`'s ORIGINAL node instead of taking `e`: there is a
|
|
107
|
+
* baseline, the original has the node, and `e` equals its baseline counterpart.
|
|
108
|
+
* merge.ts's `mergeFlow` node loop and {@link nodesTakenFromEdited} both ask this.
|
|
109
|
+
*/
|
|
110
|
+
export function isKept(e, origNodes, baseNodes, baseline) {
|
|
111
|
+
return !!baseline && origNodes.has(e.id) && nodeUnchanged(e, baseNodes.get(e.id));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Sort each finding by what merge does with its node: `refused` when the node
|
|
115
|
+
* comes from the edited flow (reason `edited`: the author changed or added it;
|
|
116
|
+
* reason `subflow`: it sits in a subflow the edit changed, which merge takes
|
|
117
|
+
* whole), `kept` when merge restores the original node — a root node the edit
|
|
118
|
+
* did not touch, or any node of a subflow the edit did not touch (#1322) — and
|
|
119
|
+
* `unplaced` when the finding names no node merge knows (an unnamed End's
|
|
120
|
+
* `.return`, a flow-level finding). The caller sorts those again with
|
|
121
|
+
* {@link splitUnplaced}: one the original's own round trip reports too is
|
|
122
|
+
* printed, one the edit introduced is refused.
|
|
123
|
+
*
|
|
124
|
+
* For instance a Slack node with no connection that the author did not touch
|
|
125
|
+
* is `kept`; the same node with a new message text is `refused` as `edited`.
|
|
126
|
+
*/
|
|
127
|
+
export function classifyDrafts(original, edited, baseline, findings) {
|
|
128
|
+
const fromEdited = new Set(nodesTakenFromEdited(original, edited, baseline));
|
|
129
|
+
const takenSubflows = new Set(subflowsTakenFromEdited(original, edited, baseline));
|
|
130
|
+
const rootIds = new Set((edited?.nodes ?? []).map((n) => n.id));
|
|
131
|
+
/** A subflow's own node ids → the key (the calling step) of the subflow holding them. */
|
|
132
|
+
const subflowOfNode = new Map();
|
|
133
|
+
for (const [key, sf] of Object.entries(edited?.subflows ?? {})) {
|
|
134
|
+
for (const n of sf?.nodes ?? [])
|
|
135
|
+
subflowOfNode.set(n.id, key);
|
|
136
|
+
}
|
|
137
|
+
const kept = [];
|
|
138
|
+
const refused = [];
|
|
139
|
+
const unplaced = [];
|
|
140
|
+
for (const f of findings) {
|
|
141
|
+
// A finding inside a subflow — placed on the calling step by `checkEdited`
|
|
142
|
+
// (`subflow` set), or a draft on one of the subflow's own nodes — is judged
|
|
143
|
+
// by the SUBFLOW: taken whole when the edit changed it, else the original's.
|
|
144
|
+
const subflow = f.subflow ?? subflowOfNode.get(f.nodeId);
|
|
145
|
+
if (subflow !== undefined) {
|
|
146
|
+
if (takenSubflows.has(subflow))
|
|
147
|
+
refused.push({ ...f, reason: 'subflow', subflow });
|
|
148
|
+
else
|
|
149
|
+
kept.push({ ...f, subflow });
|
|
150
|
+
}
|
|
151
|
+
else if (rootIds.has(f.nodeId)) {
|
|
152
|
+
if (fromEdited.has(f.nodeId))
|
|
153
|
+
refused.push({ ...f, reason: 'edited' });
|
|
154
|
+
else
|
|
155
|
+
kept.push(f);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
unplaced.push(f);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { kept, refused, unplaced };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Place each `check()` diagnostic on the node its step emitted in `editedFlow`, so
|
|
165
|
+
* {@link classifyDrafts} can sort it: a root step on its node (by label, then
|
|
166
|
+
* id); a step inside a subflow (`check` reports it as `<caller> › <step>`, one
|
|
167
|
+
* segment per level) on the deepest step the path resolves to, with `subflow`
|
|
168
|
+
* set to the deepest subflow on that path, which is what decides; a named End
|
|
169
|
+
* on its End node (#1322). A diagnostic with no step, or one no node answers
|
|
170
|
+
* to, gets nodeId `''`.
|
|
171
|
+
*/
|
|
172
|
+
export function placeDiagnostics(diagnostics, editedFlow) {
|
|
173
|
+
const byStep = new Map();
|
|
174
|
+
for (const n of editedFlow?.nodes ?? []) {
|
|
175
|
+
const label = n?.display?.label;
|
|
176
|
+
if (typeof label === 'string' && !byStep.has(label))
|
|
177
|
+
byStep.set(label, n.id);
|
|
178
|
+
}
|
|
179
|
+
const ids = new Set((editedFlow?.nodes ?? []).map((n) => n.id));
|
|
180
|
+
/** `name` as a node id in `nodes`: by label, then by id; `''` when neither answers. */
|
|
181
|
+
const resolveIn = (nodes, name) => {
|
|
182
|
+
const byLabel = nodes.find((n) => n?.display?.label === name);
|
|
183
|
+
if (byLabel)
|
|
184
|
+
return byLabel.id;
|
|
185
|
+
return nodes.some((n) => n?.id === name) ? name : '';
|
|
186
|
+
};
|
|
187
|
+
// `subflows` is flat, keyed by each caller's id at every depth, and check
|
|
188
|
+
// prefixes a finding at every level (`outer › inner › x`): the subflow that
|
|
189
|
+
// decides is the DEEPEST one on the path, since merge keeps or takes each
|
|
190
|
+
// subflow on its own (an edit inside `inner` leaves `outer` kept).
|
|
191
|
+
const place = (step) => {
|
|
192
|
+
if (step === undefined)
|
|
193
|
+
return { nodeId: '' };
|
|
194
|
+
const segments = step.split(SUBFLOW_STEP_SEPARATOR);
|
|
195
|
+
let scope = editedFlow?.nodes ?? [];
|
|
196
|
+
let subflow;
|
|
197
|
+
let nodeId = '';
|
|
198
|
+
for (const [i, segment] of segments.entries()) {
|
|
199
|
+
const id = i === 0 ? (byStep.get(segment) ?? (ids.has(segment) ? segment : '')) : resolveIn(scope, segment);
|
|
200
|
+
if (id === '')
|
|
201
|
+
break;
|
|
202
|
+
nodeId = id;
|
|
203
|
+
const child = i < segments.length - 1 ? editedFlow?.subflows?.[id] : undefined;
|
|
204
|
+
if (child === undefined)
|
|
205
|
+
break;
|
|
206
|
+
subflow = id;
|
|
207
|
+
scope = child.nodes ?? [];
|
|
208
|
+
}
|
|
209
|
+
return subflow !== undefined ? { nodeId: nodeId === '' ? subflow : nodeId, subflow } : { nodeId };
|
|
210
|
+
};
|
|
211
|
+
return diagnostics.map((d) => ({ ...d, ...place(d.step) }));
|
|
212
|
+
}
|
|
213
|
+
/** The identity of a finding for the introduced-or-not comparison: its code and message. */
|
|
214
|
+
function findingKey(f) {
|
|
215
|
+
return `${f.code}\n${f.message}`;
|
|
216
|
+
}
|
|
217
|
+
/** How many times each {@link findingKey} occurs, for {@link splitUnplaced}'s baseline. */
|
|
218
|
+
export function findingCounts(findings) {
|
|
219
|
+
const counts = new Map();
|
|
220
|
+
for (const f of findings)
|
|
221
|
+
counts.set(findingKey(f), (counts.get(findingKey(f)) ?? 0) + 1);
|
|
222
|
+
return counts;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Sort findings merge could not place on a node by whether the edit introduced
|
|
226
|
+
* them: `known` when the ORIGINAL's own round trip (`baseline`, the decompiled
|
|
227
|
+
* original compiled and checked, as {@link findingCounts}) reports the same
|
|
228
|
+
* code and message — once per occurrence, so a second copy of a finding the
|
|
229
|
+
* original has once is `introduced` — so the finding predates the edit and
|
|
230
|
+
* only prints; `introduced` otherwise, which is refused like any error on an
|
|
231
|
+
* edited node (#1322). With no baseline findings to compare against (a
|
|
232
|
+
* precompiled `--baseline` file), every one is `introduced`, as merge treated
|
|
233
|
+
* them before.
|
|
234
|
+
*/
|
|
235
|
+
export function splitUnplaced(unplaced, baseline) {
|
|
236
|
+
const known = [];
|
|
237
|
+
const introduced = [];
|
|
238
|
+
const left = new Map(baseline ?? []);
|
|
239
|
+
for (const f of unplaced) {
|
|
240
|
+
const n = left.get(findingKey(f)) ?? 0;
|
|
241
|
+
if (n > 0) {
|
|
242
|
+
left.set(findingKey(f), n - 1);
|
|
243
|
+
known.push(f);
|
|
244
|
+
}
|
|
245
|
+
else
|
|
246
|
+
introduced.push(f);
|
|
247
|
+
}
|
|
248
|
+
return { known, introduced };
|
|
249
|
+
}
|
|
250
|
+
/** How `check` joins a subflow's calling step and the step inside it in a diagnostic's `step`. */
|
|
251
|
+
const SUBFLOW_STEP_SEPARATOR = ' › ';
|
package/dist/merge-cli.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* `--baseline-from <original.flow>` makes merge build the baseline itself
|
|
10
10
|
* (decompile + compile, nothing written), and an edited `.flow.ts` makes merge
|
|
11
11
|
* compile it: both compiles tolerate DRAFT nodes, and merge refuses a draft
|
|
12
|
-
* only where it would take the node from the edited flow. See merge-compile.ts
|
|
12
|
+
* only where it would take the node from the edited flow. See merge-compile.ts
|
|
13
|
+
* and edit-scope.ts.
|
|
13
14
|
*
|
|
14
15
|
* `edited.flow` is compile(decompile(original) after your edits). `--baseline`
|
|
15
16
|
* is compile(decompile(original)) BEFORE any edit — supplying it enables the
|
|
@@ -55,7 +56,8 @@ import { flowCheckOpts, openCliLibrary } from './cli-library.js';
|
|
|
55
56
|
import { Bindings } from './bindings.js';
|
|
56
57
|
import { defaultBindingsFile } from './workdir.js';
|
|
57
58
|
import { loadNodePackages } from './node-packages.js';
|
|
58
|
-
import { baselineFromOriginal, checkEdited,
|
|
59
|
+
import { baselineFromOriginal, checkEdited, compileSourceDraftTolerant } from './merge-compile.js';
|
|
60
|
+
import { classifyDrafts, findingCounts, splitUnplaced } from './edit-scope.js';
|
|
59
61
|
const USAGE = 'usage: flow-merge <original.flow> <edited.flow | Edited.flow.ts> [-o merged.flow] '
|
|
60
62
|
+ '[--baseline <baseline.flow> | --baseline-from <original.flow>] [--library <dir>] [--bindings <file>] [--strict]';
|
|
61
63
|
/**
|
package/dist/merge-compile.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type DraftFinding, type SerializeOpts } from './serialize.js';
|
|
2
|
-
import { type CheckOpts
|
|
2
|
+
import { type CheckOpts } from './check.js';
|
|
3
|
+
import { type PlacedDiagnostic } from './edit-scope.js';
|
|
3
4
|
import type { BuiltFlow } from './flow-sdk.js';
|
|
4
5
|
/**
|
|
5
6
|
* A compile's flow, the draft refusals it reported instead of throwing, and the
|
|
@@ -30,94 +31,10 @@ export declare function compileSourceDraftTolerant(file: string, opts?: Omit<Ser
|
|
|
30
31
|
export declare function baselineFromOriginal(originalPath: string, opts?: Omit<SerializeOpts, 'draft'>, warn?: (message: string) => void): Promise<DraftTolerantCompile & {
|
|
31
32
|
built: BuiltFlow;
|
|
32
33
|
}>;
|
|
33
|
-
/**
|
|
34
|
-
* Why a finding is an error: `edited`, its node is one the author changed or
|
|
35
|
-
* added; `subflow`, it sits in a subflow the edit changed, which merge takes
|
|
36
|
-
* whole; `unplaced`, it names no node and the original's own round trip does
|
|
37
|
-
* not report it, so the edit introduced it.
|
|
38
|
-
*/
|
|
39
|
-
export type TakenReason = 'edited' | 'subflow' | 'unplaced';
|
|
40
|
-
/**
|
|
41
|
-
* A finding on a node merge takes from the edited flow, with the reason it
|
|
42
|
-
* takes it: `edited`, the author changed or added the node; `subflow`, the
|
|
43
|
-
* node sits in a subflow the edit changed, which merge takes whole (`subflow`
|
|
44
|
-
* names it).
|
|
45
|
-
*/
|
|
46
|
-
export type Refused<T> = T & {
|
|
47
|
-
reason: TakenReason;
|
|
48
|
-
subflow?: string;
|
|
49
|
-
};
|
|
50
|
-
/** A finding on a node merge restores from the original; `subflow` names the untouched subflow it sits in. */
|
|
51
|
-
export type Kept<T> = T & {
|
|
52
|
-
subflow?: string;
|
|
53
|
-
};
|
|
54
|
-
/**
|
|
55
|
-
* Sort each finding by what merge does with its node: `refused` when the node
|
|
56
|
-
* comes from the edited flow (reason `edited`: the author changed or added it;
|
|
57
|
-
* reason `subflow`: it sits in a subflow the edit changed, which merge takes
|
|
58
|
-
* whole), `kept` when merge restores the original node — a root node the edit
|
|
59
|
-
* did not touch, or any node of a subflow the edit did not touch (#1322) — and
|
|
60
|
-
* `unplaced` when the finding names no node merge knows (an unnamed End's
|
|
61
|
-
* `.return`, a flow-level finding). The caller sorts those again with
|
|
62
|
-
* {@link splitUnplaced}: one the original's own round trip reports too is
|
|
63
|
-
* printed, one the edit introduced is refused.
|
|
64
|
-
*
|
|
65
|
-
* For instance a Slack node with no connection that the author did not touch
|
|
66
|
-
* is `kept`; the same node with a new message text is `refused` as `edited`.
|
|
67
|
-
*/
|
|
68
|
-
export declare function classifyDrafts<T extends {
|
|
69
|
-
nodeId: string;
|
|
70
|
-
subflow?: string;
|
|
71
|
-
}>(original: any, edited: any, baseline: any, findings: readonly T[]): {
|
|
72
|
-
kept: Kept<T>[];
|
|
73
|
-
refused: Refused<T>[];
|
|
74
|
-
unplaced: T[];
|
|
75
|
-
};
|
|
76
|
-
/**
|
|
77
|
-
* A `check()` diagnostic, placed on the node its step emitted (`''` when it
|
|
78
|
-
* names no node); `subflow` is the calling step when the finding is inside a
|
|
79
|
-
* subflow, which then decides how it is classified.
|
|
80
|
-
*/
|
|
81
|
-
export type PlacedDiagnostic = Diagnostic & {
|
|
82
|
-
nodeId: string;
|
|
83
|
-
subflow?: string;
|
|
84
|
-
};
|
|
85
34
|
/**
|
|
86
35
|
* Run `check()` on the edited source, as `compile` does, and place each
|
|
87
|
-
* diagnostic on
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* segment per level) on the deepest step the path resolves to, with `subflow`
|
|
91
|
-
* set to the deepest subflow on that path, which is what decides; a named End
|
|
92
|
-
* on its End node (#1322). A diagnostic with no step, or one no node answers
|
|
93
|
-
* to, gets nodeId `''`.
|
|
36
|
+
* diagnostic on its node ({@link placeDiagnostics}). The `check()` call stays
|
|
37
|
+
* here, not in `edit-scope.ts`, so `check.ts` can import that module without a
|
|
38
|
+
* load-time cycle.
|
|
94
39
|
*/
|
|
95
40
|
export declare function checkEdited(built: BuiltFlow, editedFlow: any, opts: CheckOpts): PlacedDiagnostic[];
|
|
96
|
-
/** The identity of a finding for the introduced-or-not comparison: its code and message. */
|
|
97
|
-
export declare function findingKey(f: {
|
|
98
|
-
code: string;
|
|
99
|
-
message: string;
|
|
100
|
-
}): string;
|
|
101
|
-
/** How many times each {@link findingKey} occurs, for {@link splitUnplaced}'s baseline. */
|
|
102
|
-
export declare function findingCounts(findings: ReadonlyArray<{
|
|
103
|
-
code: string;
|
|
104
|
-
message: string;
|
|
105
|
-
}>): Map<string, number>;
|
|
106
|
-
/**
|
|
107
|
-
* Sort findings merge could not place on a node by whether the edit introduced
|
|
108
|
-
* them: `known` when the ORIGINAL's own round trip (`baseline`, the decompiled
|
|
109
|
-
* original compiled and checked, as {@link findingCounts}) reports the same
|
|
110
|
-
* code and message — once per occurrence, so a second copy of a finding the
|
|
111
|
-
* original has once is `introduced` — so the finding predates the edit and
|
|
112
|
-
* only prints; `introduced` otherwise, which is refused like any error on an
|
|
113
|
-
* edited node (#1322). With no baseline findings to compare against (a
|
|
114
|
-
* precompiled `--baseline` file), every one is `introduced`, as merge treated
|
|
115
|
-
* them before.
|
|
116
|
-
*/
|
|
117
|
-
export declare function splitUnplaced<T extends {
|
|
118
|
-
code: string;
|
|
119
|
-
message: string;
|
|
120
|
-
}>(unplaced: readonly T[], baseline: ReadonlyMap<string, number> | undefined): {
|
|
121
|
-
known: T[];
|
|
122
|
-
introduced: T[];
|
|
123
|
-
};
|
package/dist/merge-compile.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Both compiles tolerate draft nodes (`SerializeOpts.draft`). A draft on a node
|
|
5
5
|
* merge keeps from the original is a warning; a draft or `check()` error on a
|
|
6
6
|
* node merge takes from the edited flow is an error. Nothing draft-tolerant is
|
|
7
|
-
* written to disk.
|
|
7
|
+
* written to disk. Which nodes count as edited, and how each finding is sorted,
|
|
8
|
+
* is edit-scope.ts's.
|
|
8
9
|
*/
|
|
9
10
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
11
|
import { tmpdir } from 'node:os';
|
|
@@ -12,9 +13,9 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
12
13
|
import { decompile } from './decompile.js';
|
|
13
14
|
import { loadBuiltFlow } from './load.js';
|
|
14
15
|
import { serialize } from './serialize.js';
|
|
15
|
-
import { check } from './check.js';
|
|
16
|
-
import { nodesTakenFromEdited, subflowsTakenFromEdited } from './merge.js';
|
|
17
16
|
import { PACKAGE_NAME } from './package-name.js';
|
|
17
|
+
import { check } from './check.js';
|
|
18
|
+
import { placeDiagnostics } from './edit-scope.js';
|
|
18
19
|
/** Compile `built` with draft tolerance. Every other refusal still throws. */
|
|
19
20
|
export function compileDraftTolerant(built, opts = {}) {
|
|
20
21
|
const drafts = [];
|
|
@@ -73,143 +74,12 @@ function baselineDirectory(originalPath) {
|
|
|
73
74
|
return mkdtempSync(join(tmpdir(), 'flow-merge-baseline-'));
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Sort each finding by what merge does with its node: `refused` when the node
|
|
78
|
-
* comes from the edited flow (reason `edited`: the author changed or added it;
|
|
79
|
-
* reason `subflow`: it sits in a subflow the edit changed, which merge takes
|
|
80
|
-
* whole), `kept` when merge restores the original node — a root node the edit
|
|
81
|
-
* did not touch, or any node of a subflow the edit did not touch (#1322) — and
|
|
82
|
-
* `unplaced` when the finding names no node merge knows (an unnamed End's
|
|
83
|
-
* `.return`, a flow-level finding). The caller sorts those again with
|
|
84
|
-
* {@link splitUnplaced}: one the original's own round trip reports too is
|
|
85
|
-
* printed, one the edit introduced is refused.
|
|
86
|
-
*
|
|
87
|
-
* For instance a Slack node with no connection that the author did not touch
|
|
88
|
-
* is `kept`; the same node with a new message text is `refused` as `edited`.
|
|
89
|
-
*/
|
|
90
|
-
export function classifyDrafts(original, edited, baseline, findings) {
|
|
91
|
-
const fromEdited = new Set(nodesTakenFromEdited(original, edited, baseline));
|
|
92
|
-
const takenSubflows = new Set(subflowsTakenFromEdited(original, edited, baseline));
|
|
93
|
-
const rootIds = new Set((edited?.nodes ?? []).map((n) => n.id));
|
|
94
|
-
/** A subflow's own node ids → the key (the calling step) of the subflow holding them. */
|
|
95
|
-
const subflowOfNode = new Map();
|
|
96
|
-
for (const [key, sf] of Object.entries(edited?.subflows ?? {})) {
|
|
97
|
-
for (const n of sf?.nodes ?? [])
|
|
98
|
-
subflowOfNode.set(n.id, key);
|
|
99
|
-
}
|
|
100
|
-
const kept = [];
|
|
101
|
-
const refused = [];
|
|
102
|
-
const unplaced = [];
|
|
103
|
-
for (const f of findings) {
|
|
104
|
-
// A finding inside a subflow — placed on the calling step by `checkEdited`
|
|
105
|
-
// (`subflow` set), or a draft on one of the subflow's own nodes — is judged
|
|
106
|
-
// by the SUBFLOW: taken whole when the edit changed it, else the original's.
|
|
107
|
-
const subflow = f.subflow ?? subflowOfNode.get(f.nodeId);
|
|
108
|
-
if (subflow !== undefined) {
|
|
109
|
-
if (takenSubflows.has(subflow))
|
|
110
|
-
refused.push({ ...f, reason: 'subflow', subflow });
|
|
111
|
-
else
|
|
112
|
-
kept.push({ ...f, subflow });
|
|
113
|
-
}
|
|
114
|
-
else if (rootIds.has(f.nodeId)) {
|
|
115
|
-
if (fromEdited.has(f.nodeId))
|
|
116
|
-
refused.push({ ...f, reason: 'edited' });
|
|
117
|
-
else
|
|
118
|
-
kept.push(f);
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
unplaced.push(f);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return { kept, refused, unplaced };
|
|
125
|
-
}
|
|
126
77
|
/**
|
|
127
78
|
* Run `check()` on the edited source, as `compile` does, and place each
|
|
128
|
-
* diagnostic on
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* segment per level) on the deepest step the path resolves to, with `subflow`
|
|
132
|
-
* set to the deepest subflow on that path, which is what decides; a named End
|
|
133
|
-
* on its End node (#1322). A diagnostic with no step, or one no node answers
|
|
134
|
-
* to, gets nodeId `''`.
|
|
79
|
+
* diagnostic on its node ({@link placeDiagnostics}). The `check()` call stays
|
|
80
|
+
* here, not in `edit-scope.ts`, so `check.ts` can import that module without a
|
|
81
|
+
* load-time cycle.
|
|
135
82
|
*/
|
|
136
83
|
export function checkEdited(built, editedFlow, opts) {
|
|
137
|
-
|
|
138
|
-
for (const n of editedFlow?.nodes ?? []) {
|
|
139
|
-
const label = n?.display?.label;
|
|
140
|
-
if (typeof label === 'string' && !byStep.has(label))
|
|
141
|
-
byStep.set(label, n.id);
|
|
142
|
-
}
|
|
143
|
-
const ids = new Set((editedFlow?.nodes ?? []).map((n) => n.id));
|
|
144
|
-
/** `name` as a node id in `nodes`: by label, then by id; `''` when neither answers. */
|
|
145
|
-
const resolveIn = (nodes, name) => {
|
|
146
|
-
const byLabel = nodes.find((n) => n?.display?.label === name);
|
|
147
|
-
if (byLabel)
|
|
148
|
-
return byLabel.id;
|
|
149
|
-
return nodes.some((n) => n?.id === name) ? name : '';
|
|
150
|
-
};
|
|
151
|
-
// `subflows` is flat, keyed by each caller's id at every depth, and check
|
|
152
|
-
// prefixes a finding at every level (`outer › inner › x`): the subflow that
|
|
153
|
-
// decides is the DEEPEST one on the path, since merge keeps or takes each
|
|
154
|
-
// subflow on its own (an edit inside `inner` leaves `outer` kept).
|
|
155
|
-
const place = (step) => {
|
|
156
|
-
if (step === undefined)
|
|
157
|
-
return { nodeId: '' };
|
|
158
|
-
const segments = step.split(SUBFLOW_STEP_SEPARATOR);
|
|
159
|
-
let scope = editedFlow?.nodes ?? [];
|
|
160
|
-
let subflow;
|
|
161
|
-
let nodeId = '';
|
|
162
|
-
for (const [i, segment] of segments.entries()) {
|
|
163
|
-
const id = i === 0 ? (byStep.get(segment) ?? (ids.has(segment) ? segment : '')) : resolveIn(scope, segment);
|
|
164
|
-
if (id === '')
|
|
165
|
-
break;
|
|
166
|
-
nodeId = id;
|
|
167
|
-
const child = i < segments.length - 1 ? editedFlow?.subflows?.[id] : undefined;
|
|
168
|
-
if (child === undefined)
|
|
169
|
-
break;
|
|
170
|
-
subflow = id;
|
|
171
|
-
scope = child.nodes ?? [];
|
|
172
|
-
}
|
|
173
|
-
return subflow !== undefined ? { nodeId: nodeId === '' ? subflow : nodeId, subflow } : { nodeId };
|
|
174
|
-
};
|
|
175
|
-
return check(built, opts).map((d) => ({ ...d, ...place(d.step) }));
|
|
176
|
-
}
|
|
177
|
-
/** The identity of a finding for the introduced-or-not comparison: its code and message. */
|
|
178
|
-
export function findingKey(f) {
|
|
179
|
-
return `${f.code}\n${f.message}`;
|
|
180
|
-
}
|
|
181
|
-
/** How many times each {@link findingKey} occurs, for {@link splitUnplaced}'s baseline. */
|
|
182
|
-
export function findingCounts(findings) {
|
|
183
|
-
const counts = new Map();
|
|
184
|
-
for (const f of findings)
|
|
185
|
-
counts.set(findingKey(f), (counts.get(findingKey(f)) ?? 0) + 1);
|
|
186
|
-
return counts;
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Sort findings merge could not place on a node by whether the edit introduced
|
|
190
|
-
* them: `known` when the ORIGINAL's own round trip (`baseline`, the decompiled
|
|
191
|
-
* original compiled and checked, as {@link findingCounts}) reports the same
|
|
192
|
-
* code and message — once per occurrence, so a second copy of a finding the
|
|
193
|
-
* original has once is `introduced` — so the finding predates the edit and
|
|
194
|
-
* only prints; `introduced` otherwise, which is refused like any error on an
|
|
195
|
-
* edited node (#1322). With no baseline findings to compare against (a
|
|
196
|
-
* precompiled `--baseline` file), every one is `introduced`, as merge treated
|
|
197
|
-
* them before.
|
|
198
|
-
*/
|
|
199
|
-
export function splitUnplaced(unplaced, baseline) {
|
|
200
|
-
const known = [];
|
|
201
|
-
const introduced = [];
|
|
202
|
-
const left = new Map(baseline ?? []);
|
|
203
|
-
for (const f of unplaced) {
|
|
204
|
-
const n = left.get(findingKey(f)) ?? 0;
|
|
205
|
-
if (n > 0) {
|
|
206
|
-
left.set(findingKey(f), n - 1);
|
|
207
|
-
known.push(f);
|
|
208
|
-
}
|
|
209
|
-
else
|
|
210
|
-
introduced.push(f);
|
|
211
|
-
}
|
|
212
|
-
return { known, introduced };
|
|
84
|
+
return placeDiagnostics(check(built, opts), editedFlow);
|
|
213
85
|
}
|
|
214
|
-
/** How `check` joins a subflow's calling step and the step inside it in a diagnostic's `step`. */
|
|
215
|
-
const SUBFLOW_STEP_SEPARATOR = ' › ';
|
package/dist/merge.d.ts
CHANGED
|
@@ -87,50 +87,9 @@
|
|
|
87
87
|
* original plus the author's change to the node. See merge-embedded-agent.ts.
|
|
88
88
|
*/
|
|
89
89
|
import { type MergeSidecarSources, type SidecarWrite } from './merge-sidecars.js';
|
|
90
|
+
import { type AnyFlow } from './edit-scope.js';
|
|
90
91
|
export { MergeRefusedError, type MergeFinding } from './merge-roundtrip.js';
|
|
91
92
|
export type { MergeSidecarSources, SidecarSet, SidecarWrite } from './merge-sidecars.js';
|
|
92
|
-
interface AnyNode {
|
|
93
|
-
id: string;
|
|
94
|
-
type: string;
|
|
95
|
-
typeVersion?: string;
|
|
96
|
-
parentId?: string;
|
|
97
|
-
ui?: unknown;
|
|
98
|
-
display?: unknown;
|
|
99
|
-
inputs?: Record<string, unknown>;
|
|
100
|
-
outputs?: Record<string, unknown>;
|
|
101
|
-
[k: string]: unknown;
|
|
102
|
-
}
|
|
103
|
-
interface AnyEdge {
|
|
104
|
-
id?: string;
|
|
105
|
-
sourceNodeId: string;
|
|
106
|
-
sourcePort: string;
|
|
107
|
-
targetNodeId: string;
|
|
108
|
-
targetPort: string;
|
|
109
|
-
[k: string]: unknown;
|
|
110
|
-
}
|
|
111
|
-
interface AnyFlow {
|
|
112
|
-
id?: string;
|
|
113
|
-
name?: string;
|
|
114
|
-
version?: string;
|
|
115
|
-
nodes?: AnyNode[];
|
|
116
|
-
edges?: AnyEdge[];
|
|
117
|
-
definitions?: Array<{
|
|
118
|
-
nodeType?: string;
|
|
119
|
-
[k: string]: unknown;
|
|
120
|
-
}>;
|
|
121
|
-
variables?: unknown;
|
|
122
|
-
subflows?: Record<string, unknown>;
|
|
123
|
-
bindings?: Array<{
|
|
124
|
-
id?: string;
|
|
125
|
-
[k: string]: unknown;
|
|
126
|
-
}>;
|
|
127
|
-
metadata?: unknown;
|
|
128
|
-
solutionId?: unknown;
|
|
129
|
-
projectId?: unknown;
|
|
130
|
-
layout?: unknown;
|
|
131
|
-
runtime?: unknown;
|
|
132
|
-
[k: string]: unknown;
|
|
133
|
-
}
|
|
134
93
|
export interface MergeOptions {
|
|
135
94
|
/**
|
|
136
95
|
* compile(decompile(original)) captured BEFORE the user's edits. Enables the
|
|
@@ -172,25 +131,6 @@ export interface MergeOptions {
|
|
|
172
131
|
*/
|
|
173
132
|
strict?: boolean;
|
|
174
133
|
}
|
|
175
|
-
/**
|
|
176
|
-
* The ids of the root nodes {@link mergeFlow} takes from `edited` rather than
|
|
177
|
-
* restoring from `original`: every node that is new, or that differs from its
|
|
178
|
-
* `baseline` counterpart. With no baseline, that is every node.
|
|
179
|
-
*
|
|
180
|
-
* For instance, with a baseline equal to `edited` and every node present in
|
|
181
|
-
* `original`, the answer is `[]` — a no-edit merge keeps them all.
|
|
182
|
-
*/
|
|
183
|
-
export declare function nodesTakenFromEdited(original: AnyFlow, edited: AnyFlow, baseline?: AnyFlow): string[];
|
|
184
|
-
/**
|
|
185
|
-
* The keys of the subflows {@link mergeFlow} takes from `edited` rather than
|
|
186
|
-
* restoring from `original`: every subflow that is new, that the original does
|
|
187
|
-
* not have, or that differs from its `baseline` counterpart in any way the
|
|
188
|
-
* author can edit — a node's inputs, output sources or label, an edge, the
|
|
189
|
-
* scope's `variables` or `layout` (`subflowUnchanged`, #1322). With no
|
|
190
|
-
* baseline, that is every subflow. A kept subflow is the original's whole: its
|
|
191
|
-
* nodes, edges, variables and layout.
|
|
192
|
-
*/
|
|
193
|
-
export declare function subflowsTakenFromEdited(original: AnyFlow, edited: AnyFlow, baseline?: AnyFlow): string[];
|
|
194
134
|
export declare function mergeFlow(original: AnyFlow, edited: AnyFlow, options?: MergeOptions): AnyFlow;
|
|
195
135
|
/**
|
|
196
136
|
* `mergeFlow`, plus the agent sidecar files the merged flow needs written beside
|
package/dist/merge.js
CHANGED
|
@@ -95,119 +95,8 @@ import { placeAddedNodes } from './node-placement.js';
|
|
|
95
95
|
import { mergeAgentSidecars } from './merge-sidecars.js';
|
|
96
96
|
import { embeddedAgentFindings, isEmbeddedAgent, mergeEmbeddedAgentNode } from './merge-embedded-agent.js';
|
|
97
97
|
import { mergeObject, mergeVariables, stableJson } from './merge-variables.js';
|
|
98
|
+
import { byId, isKept, nodeUnchanged, subflowsTakenFromEdited } from './edit-scope.js';
|
|
98
99
|
export { MergeRefusedError } from './merge-roundtrip.js';
|
|
99
|
-
function byId(nodes) {
|
|
100
|
-
return new Map((nodes ?? []).map((n) => [n.id, n]));
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Whether two nodes are the SAME as far as the author is concerned — same type,
|
|
104
|
-
* same inputs and same output mappings. Layout (`ui`), `display`, `typeVersion`,
|
|
105
|
-
* and the derived parts of `outputs` (type, schema, description) are ignored,
|
|
106
|
-
* because those are exactly the parts decompile does not carry through. An
|
|
107
|
-
* output's `source` is authored, not derived: it is what an End returns, so an
|
|
108
|
-
* edit to `.return({ ... })` changes nothing else on the node. Used only to
|
|
109
|
-
* decide "did the user edit this node?" against the baseline.
|
|
110
|
-
*/
|
|
111
|
-
function nodeUnchanged(a, b) {
|
|
112
|
-
if (!a || !b)
|
|
113
|
-
return false;
|
|
114
|
-
if (a.type !== b.type)
|
|
115
|
-
return false;
|
|
116
|
-
return JSON.stringify(a.inputs ?? {}) === JSON.stringify(b.inputs ?? {})
|
|
117
|
-
&& JSON.stringify(outputSources(a)) === JSON.stringify(outputSources(b));
|
|
118
|
-
}
|
|
119
|
-
/** Each output's `source` by output name; outputs without one are left out. */
|
|
120
|
-
function outputSources(n) {
|
|
121
|
-
const sources = {};
|
|
122
|
-
for (const [name, output] of Object.entries(n.outputs ?? {})) {
|
|
123
|
-
if (output && typeof output === 'object' && 'source' in output)
|
|
124
|
-
sources[name] = output.source;
|
|
125
|
-
}
|
|
126
|
-
return sources;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* The ids of the root nodes {@link mergeFlow} takes from `edited` rather than
|
|
130
|
-
* restoring from `original`: every node that is new, or that differs from its
|
|
131
|
-
* `baseline` counterpart. With no baseline, that is every node.
|
|
132
|
-
*
|
|
133
|
-
* For instance, with a baseline equal to `edited` and every node present in
|
|
134
|
-
* `original`, the answer is `[]` — a no-edit merge keeps them all.
|
|
135
|
-
*/
|
|
136
|
-
export function nodesTakenFromEdited(original, edited, baseline) {
|
|
137
|
-
const origNodes = byId(original.nodes);
|
|
138
|
-
const baseNodes = byId(baseline?.nodes);
|
|
139
|
-
return (edited.nodes ?? []).filter((e) => !isKept(e, origNodes, baseNodes, baseline)).map((e) => e.id);
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* The keys of the subflows {@link mergeFlow} takes from `edited` rather than
|
|
143
|
-
* restoring from `original`: every subflow that is new, that the original does
|
|
144
|
-
* not have, or that differs from its `baseline` counterpart in any way the
|
|
145
|
-
* author can edit — a node's inputs, output sources or label, an edge, the
|
|
146
|
-
* scope's `variables` or `layout` (`subflowUnchanged`, #1322). With no
|
|
147
|
-
* baseline, that is every subflow. A kept subflow is the original's whole: its
|
|
148
|
-
* nodes, edges, variables and layout.
|
|
149
|
-
*/
|
|
150
|
-
export function subflowsTakenFromEdited(original, edited, baseline) {
|
|
151
|
-
return Object.keys(edited.subflows ?? {}).filter((key) => {
|
|
152
|
-
const orig = original.subflows?.[key];
|
|
153
|
-
const base = baseline?.subflows?.[key];
|
|
154
|
-
return !baseline || orig === undefined || base === undefined
|
|
155
|
-
|| !subflowUnchanged(edited.subflows[key], base);
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
/** A node's canvas label, when it has one. */
|
|
159
|
-
const labelOf = (n) => n?.display?.label;
|
|
160
|
-
/**
|
|
161
|
-
* Whether an edited subflow equals its baseline: the same node ids, each node
|
|
162
|
-
* unchanged (`nodeUnchanged`) and under the same label, the same edges by
|
|
163
|
-
* their endpoints and handles (ids are compile's own), and the same
|
|
164
|
-
* `variables` and `layout` byte for byte. The scope's variables carry what the
|
|
165
|
-
* nodes do not — a step's `{ updates }` row, a child `.input()` type, a `.var()`
|
|
166
|
-
* default — and a kept subflow is the original's whole, so any of them changing
|
|
167
|
-
* has to make the subflow a taken one.
|
|
168
|
-
*/
|
|
169
|
-
function subflowUnchanged(e, b) {
|
|
170
|
-
const eNodes = byId(e.nodes);
|
|
171
|
-
const bNodes = byId(b.nodes);
|
|
172
|
-
if (eNodes.size !== bNodes.size)
|
|
173
|
-
return false;
|
|
174
|
-
for (const [id, n] of eNodes) {
|
|
175
|
-
const base = bNodes.get(id);
|
|
176
|
-
if (!nodeUnchanged(n, base) || labelOf(n) !== labelOf(base))
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
if (JSON.stringify(e.variables ?? null) !== JSON.stringify(b.variables ?? null))
|
|
180
|
-
return false;
|
|
181
|
-
// `layout.edges` is keyed by edge id, and ids come from one file-wide
|
|
182
|
-
// counter: a step added before the call shifts every id in the subflow
|
|
183
|
-
// without touching it. So routes are compared by the endpoints the id names
|
|
184
|
-
// (as the edges themselves are), and `layout.nodes` as it is.
|
|
185
|
-
if (JSON.stringify(layoutByEndpoints(e)) !== JSON.stringify(layoutByEndpoints(b)))
|
|
186
|
-
return false;
|
|
187
|
-
const eEdges = (e.edges ?? []).map(edgeKey).sort();
|
|
188
|
-
const bEdges = (b.edges ?? []).map(edgeKey).sort();
|
|
189
|
-
return eEdges.length === bEdges.length && eEdges.every((k, i) => k === bEdges[i]);
|
|
190
|
-
}
|
|
191
|
-
/** `<source>@<port>-><target>@<port>`: an edge's identity apart from its id. */
|
|
192
|
-
const edgeKey = (x) => `${x.sourceNodeId}@${x.sourcePort}->${x.targetNodeId}@${x.targetPort}`;
|
|
193
|
-
/** A scope's layout with its `edges` re-keyed by endpoints, so an id shift does not read as a change. */
|
|
194
|
-
function layoutByEndpoints(sf) {
|
|
195
|
-
const layout = sf.layout;
|
|
196
|
-
if (!layout || typeof layout !== 'object')
|
|
197
|
-
return layout ?? null;
|
|
198
|
-
const byId = new Map((sf.edges ?? []).map((x) => [x.id, edgeKey(x)]));
|
|
199
|
-
const { edges, ...rest } = layout;
|
|
200
|
-
const routes = Object.entries(edges ?? {}).map(([id, route]) => [byId.get(id) ?? id, route]).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
201
|
-
return { ...rest, edges: Object.fromEntries(routes) };
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Whether merge restores `e`'s ORIGINAL node instead of taking `e`: there is a
|
|
205
|
-
* baseline, the original has the node, and `e` equals its baseline counterpart.
|
|
206
|
-
* {@link mergeFlow}'s node loop and {@link nodesTakenFromEdited} both ask this.
|
|
207
|
-
*/
|
|
208
|
-
function isKept(e, origNodes, baseNodes, baseline) {
|
|
209
|
-
return !!baseline && origNodes.has(e.id) && nodeUnchanged(e, baseNodes.get(e.id));
|
|
210
|
-
}
|
|
211
100
|
/**
|
|
212
101
|
* Give an edited node the original's position, by id.
|
|
213
102
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.58.
|
|
3
|
+
"version": "6.58.1",
|
|
4
4
|
"description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.uipath.com/maestro",
|
|
@@ -96,5 +96,5 @@
|
|
|
96
96
|
"@types/node": "^22.7.0",
|
|
97
97
|
"esbuild": "^0.28.1"
|
|
98
98
|
},
|
|
99
|
-
"gitref": "
|
|
99
|
+
"gitref": "5109abb1e51b0990a2318bf03312a805eb194fbf"
|
|
100
100
|
}
|