@uipath/maestro-builder-sdk 6.20.5 → 6.20.6
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/api-index.md +18 -18
- package/dist/api-members.md +74 -74
- package/dist/check.js +15 -3
- package/dist/core/js-code-spans.d.ts +24 -0
- package/dist/core/js-code-spans.js +148 -0
- package/dist/decompile.js +107 -5
- package/dist/flow-output-backing.d.ts +53 -0
- package/dist/flow-output-backing.js +200 -0
- package/dist/flow-sdk.d.ts +15 -0
- package/dist/flow-sdk.js +10 -0
- package/dist/flow-variables.d.ts +6 -4
- package/dist/flow-variables.js +6 -4
- package/dist/serialize.js +69 -22
- package/package.json +2 -2
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of a JavaScript expression or script body that are code.
|
|
3
|
+
*
|
|
4
|
+
* Short version: `$vars.summary` inside a quoted string is text, not a read.
|
|
5
|
+
* A rewrite that renames `$vars.<name>` (a step's node id, an output's backing
|
|
6
|
+
* Variable, #1092) must leave string literals, comments and regex literals
|
|
7
|
+
* alone, or it changes what the flow does:
|
|
8
|
+
*
|
|
9
|
+
* ```js
|
|
10
|
+
* return "$vars.summary" + $vars.summary;
|
|
11
|
+
* // ^^^^^^^^^^^^^^^ text: kept ^^^^^^^^^^^^^ code: renamed
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* A template literal's text is kept too, and its `${…}` holes are code.
|
|
15
|
+
*
|
|
16
|
+
* This is a scanner, not a parser. It tells a regex literal from a division by
|
|
17
|
+
* the character before the `/` (the usual heuristic), so `return /"/` is read
|
|
18
|
+
* as a division. Nothing in a flow's expressions depends on that case.
|
|
19
|
+
*/
|
|
20
|
+
/** Characters after which a `/` starts a regex literal, not a division. */
|
|
21
|
+
const REGEX_AFTER = new Set('(,=:[!&|?{};+-*%<>~^'.split(''));
|
|
22
|
+
/**
|
|
23
|
+
* `text` with `fn` applied to each run of code, and every string, template
|
|
24
|
+
* text, comment and regex literal copied as written.
|
|
25
|
+
*/
|
|
26
|
+
export function mapJsCode(text, fn) {
|
|
27
|
+
let out = '';
|
|
28
|
+
const n = text.length;
|
|
29
|
+
const flushCode = (from, to) => { if (to > from)
|
|
30
|
+
out += fn(text.slice(from, to)); };
|
|
31
|
+
/** Index just past the quoted string that opens at `i`. */
|
|
32
|
+
const endQuoted = (i, q) => {
|
|
33
|
+
let j = i + 1;
|
|
34
|
+
while (j < n && text[j] !== q && text[j] !== '\n')
|
|
35
|
+
j += text[j] === '\\' ? 2 : 1;
|
|
36
|
+
return Math.min(j + 1, n);
|
|
37
|
+
};
|
|
38
|
+
/** Index just past the regex literal that opens at `i`. */
|
|
39
|
+
const endRegex = (i) => {
|
|
40
|
+
let j = i + 1;
|
|
41
|
+
let inClass = false;
|
|
42
|
+
while (j < n && text[j] !== '\n') {
|
|
43
|
+
const c = text[j];
|
|
44
|
+
if (c === '\\') {
|
|
45
|
+
j += 2;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (c === '[')
|
|
49
|
+
inClass = true;
|
|
50
|
+
else if (c === ']')
|
|
51
|
+
inClass = false;
|
|
52
|
+
else if (c === '/' && !inClass) {
|
|
53
|
+
j++;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
j++;
|
|
57
|
+
}
|
|
58
|
+
while (j < n && /[A-Za-z]/.test(text[j]))
|
|
59
|
+
j++;
|
|
60
|
+
return Math.min(j, n);
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Scan code from `i`. Inside a template hole, stop at its closing `}` and
|
|
64
|
+
* return that index; otherwise run to the end.
|
|
65
|
+
*/
|
|
66
|
+
const scanCode = (i, inHole) => {
|
|
67
|
+
let start = i;
|
|
68
|
+
let depth = 0;
|
|
69
|
+
let prev = '';
|
|
70
|
+
while (i < n) {
|
|
71
|
+
const c = text[i];
|
|
72
|
+
if (c === '"' || c === "'") {
|
|
73
|
+
flushCode(start, i);
|
|
74
|
+
const j = endQuoted(i, c);
|
|
75
|
+
out += text.slice(i, j);
|
|
76
|
+
i = start = j;
|
|
77
|
+
prev = c;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (c === '`') {
|
|
81
|
+
flushCode(start, i);
|
|
82
|
+
i = start = scanTemplate(i);
|
|
83
|
+
prev = c;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (c === '/' && (text[i + 1] === '/' || text[i + 1] === '*')) {
|
|
87
|
+
flushCode(start, i);
|
|
88
|
+
const close = text[i + 1] === '/' ? text.indexOf('\n', i) : text.indexOf('*/', i + 2);
|
|
89
|
+
const j = close < 0 ? n : text[i + 1] === '/' ? close : close + 2;
|
|
90
|
+
out += text.slice(i, j);
|
|
91
|
+
i = start = j;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (c === '/' && (prev === '' || REGEX_AFTER.has(prev))) {
|
|
95
|
+
flushCode(start, i);
|
|
96
|
+
const j = endRegex(i);
|
|
97
|
+
out += text.slice(i, j);
|
|
98
|
+
i = start = j;
|
|
99
|
+
prev = '/';
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (inHole) {
|
|
103
|
+
if (c === '{')
|
|
104
|
+
depth++;
|
|
105
|
+
else if (c === '}') {
|
|
106
|
+
if (depth === 0) {
|
|
107
|
+
flushCode(start, i);
|
|
108
|
+
return i;
|
|
109
|
+
}
|
|
110
|
+
depth--;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!/\s/.test(c))
|
|
114
|
+
prev = c;
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
flushCode(start, i);
|
|
118
|
+
return i;
|
|
119
|
+
};
|
|
120
|
+
/** Copy the template literal that opens at `i`, mapping its holes; return the index past it. */
|
|
121
|
+
const scanTemplate = (i) => {
|
|
122
|
+
let start = i;
|
|
123
|
+
let j = i + 1;
|
|
124
|
+
while (j < n) {
|
|
125
|
+
const c = text[j];
|
|
126
|
+
if (c === '\\') {
|
|
127
|
+
j += 2;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (c === '`') {
|
|
131
|
+
out += text.slice(start, j + 1);
|
|
132
|
+
return j + 1;
|
|
133
|
+
}
|
|
134
|
+
if (c === '$' && text[j + 1] === '{') {
|
|
135
|
+
out += text.slice(start, j + 2);
|
|
136
|
+
j = scanCode(j + 2, true);
|
|
137
|
+
start = j; // the hole's `}` opens the next verbatim run
|
|
138
|
+
j++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
j++;
|
|
142
|
+
}
|
|
143
|
+
out += text.slice(start, n);
|
|
144
|
+
return n;
|
|
145
|
+
};
|
|
146
|
+
scanCode(0, false);
|
|
147
|
+
return out;
|
|
148
|
+
}
|
package/dist/decompile.js
CHANGED
|
@@ -40,6 +40,8 @@ import { contextIndexSlug, parseContextIndexType } from './core/context-index.js
|
|
|
40
40
|
import { wireExprBody } from './core/wire-expr.js';
|
|
41
41
|
import { parseIxpProjectType } from './core/actions.js';
|
|
42
42
|
import { readFlowGlobals } from './flow-variables.js';
|
|
43
|
+
import { outputBackingGlobal, outputBackingName } from './flow-output-backing.js';
|
|
44
|
+
import { mapJsCode } from './core/js-code-spans.js';
|
|
43
45
|
import { missingBindingLabels } from './core/binding-messages.js';
|
|
44
46
|
import { prepareCommand } from './core/cli-spelling.js';
|
|
45
47
|
import { OUTCOME_HANDLE_NODE_TYPES, outcomeSlug } from './core/hitl-routing.js';
|
|
@@ -2342,6 +2344,7 @@ function armCb(param, segs) {
|
|
|
2342
2344
|
*/
|
|
2343
2345
|
function returnSeg(node, ctx) {
|
|
2344
2346
|
const updates = ctx.flow.variables?.variableUpdates?.[node.id] ?? [];
|
|
2347
|
+
const outputIds = new Set(readFlowGlobals(ctx.flow.variables).filter((g) => g.direction === 'out').map((g) => g.id));
|
|
2345
2348
|
const opts = returnOptsArg(node);
|
|
2346
2349
|
const entries = updates.map((u) => `${key(u.variableId)}: ${emitVal(u.expression, ctx.imp, ctx.inputNames)}`);
|
|
2347
2350
|
const written = new Set(updates.map((u) => u.variableId));
|
|
@@ -2350,7 +2353,7 @@ function returnSeg(node, ctx) {
|
|
|
2350
2353
|
continue;
|
|
2351
2354
|
const source = out?.source;
|
|
2352
2355
|
const raw = unwrapSource(source);
|
|
2353
|
-
if (raw === undefined || isIdentityEndSource(raw, name))
|
|
2356
|
+
if (raw === undefined || isIdentityEndSource(raw, name, outputIds.has(name)))
|
|
2354
2357
|
continue;
|
|
2355
2358
|
entries.push(`${key(name)}: ${emitVal(source, ctx.imp, ctx.inputNames)}`);
|
|
2356
2359
|
}
|
|
@@ -2358,10 +2361,15 @@ function returnSeg(node, ctx) {
|
|
|
2358
2361
|
return `.return({}${opts})`;
|
|
2359
2362
|
return `.return({ ${entries.join(', ')} }${opts})`;
|
|
2360
2363
|
}
|
|
2361
|
-
/**
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2364
|
+
/**
|
|
2365
|
+
* The fill-in mapping `endOutputs` writes for a variable no `.return` named:
|
|
2366
|
+
* `=js:$vars.<name>`, read through the backing Variable `foldOutputBacking`
|
|
2367
|
+
* has already folded back. An output with no backing Variable fills in as
|
|
2368
|
+
* `null` (#1092), since nothing but `.return()` sets it.
|
|
2369
|
+
*/
|
|
2370
|
+
function isIdentityEndSource(raw, name, isOutput = false) {
|
|
2371
|
+
const body = (raw.startsWith('=js:') ? raw.slice(4) : raw).trim();
|
|
2372
|
+
return body === `$vars.${name}` || (isOutput && body === 'null');
|
|
2365
2373
|
}
|
|
2366
2374
|
/**
|
|
2367
2375
|
* The options argument for a `.return(...)` — `version`, plus the End's `name`
|
|
@@ -3131,7 +3139,101 @@ function slugId(flow) {
|
|
|
3131
3139
|
const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
3132
3140
|
return slug || 'flow';
|
|
3133
3141
|
}
|
|
3142
|
+
// ─── Output backing Variables (#1092) ─────────────────────────────────────────
|
|
3143
|
+
/** Sorted-key JSON, so two globals compare equal whatever their key order. */
|
|
3144
|
+
function canonicalJson(v) {
|
|
3145
|
+
if (Array.isArray(v))
|
|
3146
|
+
return `[${v.map(canonicalJson).join(',')}]`;
|
|
3147
|
+
if (v && typeof v === 'object') {
|
|
3148
|
+
const o = v;
|
|
3149
|
+
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(',')}}`;
|
|
3150
|
+
}
|
|
3151
|
+
return JSON.stringify(v);
|
|
3152
|
+
}
|
|
3153
|
+
/**
|
|
3154
|
+
* The wire fields whose plain-string value is JavaScript (no `=js:` prefix):
|
|
3155
|
+
* a script body, a decision or switch `expression`, a loop's `condition` and
|
|
3156
|
+
* `completionCondition`, an HTTP branch's `conditionExpression`, a transform's
|
|
3157
|
+
* `collection`. `serialize` renames `$vars` reads in exactly these and in
|
|
3158
|
+
* `=js:` values.
|
|
3159
|
+
*/
|
|
3160
|
+
const RAW_JS_FIELDS = new Set(['script', 'expression', 'condition', 'completionCondition', 'conditionExpression', 'collection']);
|
|
3161
|
+
/**
|
|
3162
|
+
* Every `$vars.<from>` read (the whole root, not a prefix of a longer one) in
|
|
3163
|
+
* `v`'s code, read as `$vars.<to>`. Code is a `=js:` wire value, a plain
|
|
3164
|
+
* `$vars.` path, or a `RAW_JS_FIELDS` value; in each, only the code spans are
|
|
3165
|
+
* rewritten. A label, a literal input, or a quoted `"$vars.<from>"` inside a
|
|
3166
|
+
* script stays as written, the way `serialize` left it (#1092 review).
|
|
3167
|
+
*/
|
|
3168
|
+
function renameVarRoot(v, from, to) {
|
|
3169
|
+
const re = new RegExp(`\\$vars\\.${from.replace(/[$]/g, '\\$&')}(?![A-Za-z0-9_$])`, 'g');
|
|
3170
|
+
const inCode = (js) => mapJsCode(js, (code) => code.replace(re, `$vars.${to}`));
|
|
3171
|
+
const walk = (x, key) => {
|
|
3172
|
+
if (typeof x === 'string') {
|
|
3173
|
+
if (x.startsWith('=js:'))
|
|
3174
|
+
return '=js:' + inCode(x.slice(4));
|
|
3175
|
+
if (x.startsWith('$vars.') || (key !== undefined && RAW_JS_FIELDS.has(key)))
|
|
3176
|
+
return inCode(x);
|
|
3177
|
+
return x;
|
|
3178
|
+
}
|
|
3179
|
+
if (Array.isArray(x))
|
|
3180
|
+
return x.map((y) => walk(y, key));
|
|
3181
|
+
if (x && typeof x === 'object')
|
|
3182
|
+
return Object.fromEntries(Object.entries(x).map(([k, y]) => [k, walk(y, k)]));
|
|
3183
|
+
return x;
|
|
3184
|
+
};
|
|
3185
|
+
return walk(v);
|
|
3186
|
+
}
|
|
3187
|
+
/**
|
|
3188
|
+
* Undo the backing-Variable lowering `serialize` applies to an output the flow
|
|
3189
|
+
* sets from inside (`flow-output-backing.ts`): fold the `inout` `<output>Value`
|
|
3190
|
+
* global back into its output, so the source says `{ updates: { summary } }`
|
|
3191
|
+
* and `v('summary')` again and recompiling emits the same bytes.
|
|
3192
|
+
*
|
|
3193
|
+
* A pair is folded only when the Variable is exactly what `serialize` would
|
|
3194
|
+
* emit for that output (`outputBackingGlobal`): same id, type, default,
|
|
3195
|
+
* subType and schema, no description, no trigger. Any other `inout` global,
|
|
3196
|
+
* one a person named `summaryValue` with a description say, stays a `.var()`.
|
|
3197
|
+
* Applied to each subflow body too, against that body's own globals.
|
|
3198
|
+
*/
|
|
3199
|
+
function foldOutputBacking(flow) {
|
|
3200
|
+
const subflows = flow.subflows
|
|
3201
|
+
? Object.fromEntries(Object.entries(flow.subflows).map(([k, c]) => [k, foldOutputBacking(c)]))
|
|
3202
|
+
: undefined;
|
|
3203
|
+
let out = subflows ? { ...flow, subflows } : flow;
|
|
3204
|
+
const globals = Array.isArray(flow.variables?.globals) ? flow.variables.globals : [];
|
|
3205
|
+
for (const o of globals.filter((g) => g.direction === 'out')) {
|
|
3206
|
+
const id = outputBackingName(o.id);
|
|
3207
|
+
const b = globals.find((g) => g.id === id && g.direction === 'inout');
|
|
3208
|
+
if (!b)
|
|
3209
|
+
continue;
|
|
3210
|
+
const expected = outputBackingGlobal({ name: o.id, type: o.type, default: o.defaultValue, subType: o.subType, schema: o.schema }, id);
|
|
3211
|
+
if (canonicalJson(b) !== canonicalJson(expected))
|
|
3212
|
+
continue;
|
|
3213
|
+
const vars = out.variables;
|
|
3214
|
+
const updates = Object.fromEntries(Object.entries(vars.variableUpdates ?? {}).map(([node, rows]) => [
|
|
3215
|
+
node, rows.map((r) => (r.variableId === id ? { ...r, variableId: o.id } : r)),
|
|
3216
|
+
]));
|
|
3217
|
+
const nodes = (out.nodes ?? []).map((n) => {
|
|
3218
|
+
if (n.type !== 'core.control.end' || !n.outputs || !(id in n.outputs))
|
|
3219
|
+
return n;
|
|
3220
|
+
const { [id]: _dropped, ...rest } = n.outputs;
|
|
3221
|
+
return { ...n, outputs: rest };
|
|
3222
|
+
});
|
|
3223
|
+
out = {
|
|
3224
|
+
...out,
|
|
3225
|
+
nodes: renameVarRoot(nodes, id, o.id),
|
|
3226
|
+
variables: {
|
|
3227
|
+
...vars,
|
|
3228
|
+
globals: (vars.globals ?? []).filter((g) => g !== b && g.id !== id),
|
|
3229
|
+
variableUpdates: renameVarRoot(updates, id, o.id),
|
|
3230
|
+
},
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
return out;
|
|
3234
|
+
}
|
|
3134
3235
|
export function decompile(flow, options = {}) {
|
|
3236
|
+
flow = foldOutputBacking(flow);
|
|
3135
3237
|
const importSpecifier = options.importSpecifier ?? './flow-sdk.js';
|
|
3136
3238
|
runStrict = options.bestEffort === false;
|
|
3137
3239
|
runWarn = options.warn;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { BuiltFlow, Step, VarDecl } from './flow-sdk.js';
|
|
2
|
+
/** The suffix of a backing Variable's id: `summary` → `summaryValue`. */
|
|
3
|
+
export declare const OUTPUT_BACKING_SUFFIX = "Value";
|
|
4
|
+
/** The id of the Variable that backs output `output`. */
|
|
5
|
+
export declare function outputBackingName(output: string): string;
|
|
6
|
+
/** One reason a backing Variable cannot be emitted. */
|
|
7
|
+
export interface OutputBackingProblem {
|
|
8
|
+
code: 'OUTPUT_BACKING_COLLISION';
|
|
9
|
+
name: string;
|
|
10
|
+
message: string;
|
|
11
|
+
suggestion?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Which outputs get a backing Variable, in output declaration order. */
|
|
14
|
+
export interface OutputBackingPlan {
|
|
15
|
+
/** Output name → backing Variable id. */
|
|
16
|
+
backing: Map<string, string>;
|
|
17
|
+
/**
|
|
18
|
+
* The same pairs, minus outputs that share their name with a step. Those
|
|
19
|
+
* `$vars.<name>` reads are the step's output (`out('slug')` with an output
|
|
20
|
+
* also called `slug`), so only these are redirected to the Variable.
|
|
21
|
+
*/
|
|
22
|
+
reads: Map<string, string>;
|
|
23
|
+
problems: OutputBackingProblem[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The variable names one scope's steps write with `{ updates }`, the `$vars`
|
|
27
|
+
* roots its expressions and script bodies read, and its step names. A
|
|
28
|
+
* subflow's child flow is skipped: its globals are its own.
|
|
29
|
+
*/
|
|
30
|
+
export declare function scopeVariableUse(steps: readonly Step[]): {
|
|
31
|
+
written: Set<string>;
|
|
32
|
+
read: Set<string>;
|
|
33
|
+
stepNames: Set<string>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Plan the backing Variables for one scope (the flow, or one subflow body).
|
|
37
|
+
*
|
|
38
|
+
* @param built - The scope's flow.
|
|
39
|
+
* @param outputs - Its outputs after the `.output()` + `.var()` merge (`planFlowGlobals`).
|
|
40
|
+
* @param vars - The `.var()`s it emits, the SDK-declared `{ shared }` ones included.
|
|
41
|
+
* @param sharedOutputs - The outputs a `{ shared }` input feeds.
|
|
42
|
+
*/
|
|
43
|
+
export declare function planOutputBacking(built: Pick<BuiltFlow, 'inputs' | 'steps' | 'entryPoints' | 'triggerId'>, outputs: readonly VarDecl[], vars: readonly {
|
|
44
|
+
name: string;
|
|
45
|
+
}[], sharedOutputs?: ReadonlySet<string>): OutputBackingPlan;
|
|
46
|
+
/** The `inout` global that backs output `o`: its type and value metadata, no description. */
|
|
47
|
+
export declare function outputBackingGlobal(o: {
|
|
48
|
+
name: string;
|
|
49
|
+
type: string;
|
|
50
|
+
default?: unknown;
|
|
51
|
+
subType?: string;
|
|
52
|
+
schema?: Record<string, unknown>;
|
|
53
|
+
}, id: string): Record<string, unknown>;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backing variables for flow outputs that a step sets (flow-builder-sdk#1092).
|
|
3
|
+
*
|
|
4
|
+
* Short version: in Studio Web an Output (`direction: 'out'`) is set ONLY by an
|
|
5
|
+
* End node's output mapping. "Update Variable" offers only Variables
|
|
6
|
+
* (`direction: 'inout'`), and expressions can't read an Output. So when an
|
|
7
|
+
* author's source sets an output from inside the flow, the SDK emits the shape a
|
|
8
|
+
* Studio Web user builds by hand: a Variable that holds the running value, and
|
|
9
|
+
* an End mapping that copies it into the Output.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* flow('probe').output({ summary: types.string })
|
|
13
|
+
* .step('work', script({ … }), { updates: { summary: out('work') } })
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* emits
|
|
17
|
+
*
|
|
18
|
+
* ```
|
|
19
|
+
* globals: summary (out), summaryValue (inout)
|
|
20
|
+
* variableUpdates: work → summaryValue = $vars.work.output
|
|
21
|
+
* end.outputs: summary ← $vars.summaryValue, summaryValue ← $vars.summaryValue
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* The oracle (flow-workbench `develop`):
|
|
25
|
+
* - `VariablesContext.tsx` `getWritableVariables` / `useWritableVariables`:
|
|
26
|
+
* the Update Variable editor lists `direction === 'inout'` only, and creates
|
|
27
|
+
* new rows as `inout` (`VariableUpdateEditor.tsx`).
|
|
28
|
+
* - `VariablesContext.tsx` `getReadableVariables` / `filterReadableVariables`:
|
|
29
|
+
* expression editors drop `direction === 'out'`; the validator's `$vars`
|
|
30
|
+
* declarations (`generate-variable-declarations.ts`) hold `in` and `inout`.
|
|
31
|
+
* Reading `$vars.<output>` is `EXPRESSION_DIAGNOSTIC: Property '<output>'
|
|
32
|
+
* does not exist`.
|
|
33
|
+
* - `end-node-sets-output.flow` / `end-node-different-branches.flow`: an End
|
|
34
|
+
* sets an Output from an expression, `outputs.<id>.source`, and writes no
|
|
35
|
+
* variable update.
|
|
36
|
+
* - The product has no naming rule for the Variable (the dialog checks only
|
|
37
|
+
* identifier syntax and uniqueness against `useReservedIds`). The SDK names
|
|
38
|
+
* it `<output>Value`, the same suffix its `{ shared }` suggestion uses, and
|
|
39
|
+
* refuses a source that already uses that name for something else.
|
|
40
|
+
*
|
|
41
|
+
* An output gets a backing Variable when the flow's own source sets or reads it
|
|
42
|
+
* outside a `.return()` value:
|
|
43
|
+
* - a step's `{ updates }` (any step kind, `.return(…, { updates })` included);
|
|
44
|
+
* - a `{ shared }` input that feeds it;
|
|
45
|
+
* - a declared `default` (including the one a same-named `.var()` gives it,
|
|
46
|
+
* flow-builder-sdk#899), because the default is the value an End returns when
|
|
47
|
+
* no step set it;
|
|
48
|
+
* - an expression or script body that reads `$vars.<output>` in code (unless a
|
|
49
|
+
* step has the output's name: then `$vars.<name>` is that step's output). A
|
|
50
|
+
* label, a literal input, or a quoted `"$vars.<output>"` inside a script is
|
|
51
|
+
* text and reads nothing.
|
|
52
|
+
*
|
|
53
|
+
* An output that only `.return({ … })` sets needs none: each End maps it from
|
|
54
|
+
* the returned expression, and an End that does not name it maps `null`, which
|
|
55
|
+
* is what the old `$vars.<output>` read evaluated to on that path.
|
|
56
|
+
*/
|
|
57
|
+
import { Expr } from './core/expr.js';
|
|
58
|
+
import { mapJsCode } from './core/js-code-spans.js';
|
|
59
|
+
/** The suffix of a backing Variable's id: `summary` → `summaryValue`. */
|
|
60
|
+
export const OUTPUT_BACKING_SUFFIX = 'Value';
|
|
61
|
+
/** The id of the Variable that backs output `output`. */
|
|
62
|
+
export function outputBackingName(output) {
|
|
63
|
+
return `${output}${OUTPUT_BACKING_SUFFIX}`;
|
|
64
|
+
}
|
|
65
|
+
/** A `$vars.<root>` read: the root identifier, ended by anything that cannot continue it. */
|
|
66
|
+
const VARS_ROOT_RE = /\$vars\.([A-Za-z_$][A-Za-z0-9_$]*)/g;
|
|
67
|
+
/** The `$vars` roots that `js`'s code reads; a quoted `"$vars.x"` is text, not a read. */
|
|
68
|
+
function readsOf(js, into) {
|
|
69
|
+
mapJsCode(js, (code) => {
|
|
70
|
+
for (const m of code.matchAll(VARS_ROOT_RE))
|
|
71
|
+
into.add(m[1]);
|
|
72
|
+
return code;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** A wire expression (`=js:…`), as a raw node carries one. */
|
|
76
|
+
const WIRE_JS_PREFIX = '=js:';
|
|
77
|
+
/** A child flow (a subflow's body) is its own scope with its own globals. */
|
|
78
|
+
function isBuiltFlow(v) {
|
|
79
|
+
const o = v;
|
|
80
|
+
return Array.isArray(o.steps) && Array.isArray(o.outputs) && Array.isArray(o.inputs) && Array.isArray(o.vars);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The variable names one scope's steps write with `{ updates }`, the `$vars`
|
|
84
|
+
* roots its expressions and script bodies read, and its step names. A
|
|
85
|
+
* subflow's child flow is skipped: its globals are its own.
|
|
86
|
+
*/
|
|
87
|
+
export function scopeVariableUse(steps) {
|
|
88
|
+
const written = new Set();
|
|
89
|
+
const read = new Set();
|
|
90
|
+
const stepNames = new Set();
|
|
91
|
+
const seen = new WeakSet();
|
|
92
|
+
const visit = (val) => {
|
|
93
|
+
// A plain string is a label, a prompt or a literal input, never a read;
|
|
94
|
+
// only a raw node's `=js:` wire expression is code (#1092 review).
|
|
95
|
+
if (typeof val === 'string') {
|
|
96
|
+
if (val.startsWith(WIRE_JS_PREFIX))
|
|
97
|
+
readsOf(val.slice(WIRE_JS_PREFIX.length), read);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (val === null || typeof val !== 'object')
|
|
101
|
+
return;
|
|
102
|
+
if (seen.has(val))
|
|
103
|
+
return;
|
|
104
|
+
seen.add(val);
|
|
105
|
+
if (val instanceof Expr) {
|
|
106
|
+
if (!val.literal)
|
|
107
|
+
readsOf(val.js, read);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (isBuiltFlow(val))
|
|
111
|
+
return;
|
|
112
|
+
if (Array.isArray(val)) {
|
|
113
|
+
for (const v of val)
|
|
114
|
+
visit(v);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const rec = val;
|
|
118
|
+
// A script body is a plain string that is code (`script({ code })`).
|
|
119
|
+
if (rec.kind === 'script' && typeof rec.inputs?.code === 'string') {
|
|
120
|
+
readsOf(rec.inputs.code, read);
|
|
121
|
+
}
|
|
122
|
+
if (typeof rec.kind === 'string') {
|
|
123
|
+
// A step. Its name is a node id (a `.return()`'s synthetic one is not).
|
|
124
|
+
if (typeof rec.name === 'string' && rec.kind !== 'return')
|
|
125
|
+
stepNames.add(rec.name);
|
|
126
|
+
const updates = rec.options?.updates;
|
|
127
|
+
if (updates && typeof updates === 'object' && !(updates instanceof Expr)) {
|
|
128
|
+
for (const k of Object.keys(updates))
|
|
129
|
+
written.add(k);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const v of Object.values(rec))
|
|
133
|
+
visit(v);
|
|
134
|
+
};
|
|
135
|
+
for (const s of steps)
|
|
136
|
+
visit(s);
|
|
137
|
+
return { written, read, stepNames };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Plan the backing Variables for one scope (the flow, or one subflow body).
|
|
141
|
+
*
|
|
142
|
+
* @param built - The scope's flow.
|
|
143
|
+
* @param outputs - Its outputs after the `.output()` + `.var()` merge (`planFlowGlobals`).
|
|
144
|
+
* @param vars - The `.var()`s it emits, the SDK-declared `{ shared }` ones included.
|
|
145
|
+
* @param sharedOutputs - The outputs a `{ shared }` input feeds.
|
|
146
|
+
*/
|
|
147
|
+
export function planOutputBacking(built, outputs, vars, sharedOutputs = new Set()) {
|
|
148
|
+
const steps = [...built.steps, ...(built.entryPoints ?? []).flatMap((ep) => ep.steps)];
|
|
149
|
+
const use = scopeVariableUse(steps);
|
|
150
|
+
const taken = new Map();
|
|
151
|
+
const claim = (name, what) => { if (!taken.has(name))
|
|
152
|
+
taken.set(name, what); };
|
|
153
|
+
for (const i of built.inputs)
|
|
154
|
+
claim(i.name, `input "${i.name}"`);
|
|
155
|
+
for (const ep of built.entryPoints ?? []) {
|
|
156
|
+
claim(ep.id, `entry point "${ep.id}"`);
|
|
157
|
+
for (const i of ep.inputs)
|
|
158
|
+
claim(i.name, `input "${i.name}"`);
|
|
159
|
+
}
|
|
160
|
+
for (const o of outputs)
|
|
161
|
+
claim(o.name, `output "${o.name}"`);
|
|
162
|
+
for (const v of vars)
|
|
163
|
+
claim(v.name, `variable "${v.name}"`);
|
|
164
|
+
for (const s of use.stepNames)
|
|
165
|
+
claim(s, `step "${s}"`);
|
|
166
|
+
claim(built.triggerId ?? 'start', `trigger "${built.triggerId ?? 'start'}"`);
|
|
167
|
+
const backing = new Map();
|
|
168
|
+
const problems = [];
|
|
169
|
+
for (const o of outputs) {
|
|
170
|
+
const readsIt = use.read.has(o.name) && !use.stepNames.has(o.name);
|
|
171
|
+
const needs = use.written.has(o.name) || readsIt || sharedOutputs.has(o.name) || o.default !== undefined;
|
|
172
|
+
if (!needs)
|
|
173
|
+
continue;
|
|
174
|
+
const id = outputBackingName(o.name);
|
|
175
|
+
const holder = taken.get(id);
|
|
176
|
+
if (holder) {
|
|
177
|
+
problems.push({
|
|
178
|
+
code: 'OUTPUT_BACKING_COLLISION', name: o.name,
|
|
179
|
+
message: `Output "${o.name}" is set inside the flow, so compile backs it with a Variable named "${id}": `
|
|
180
|
+
+ `Studio Web sets an Output only on an End node, and Update Variable writes only Variables. `
|
|
181
|
+
+ `But "${id}" is already the ${holder}, and the .flow keeps one id per variable or step.`,
|
|
182
|
+
suggestion: `Rename the ${holder}.`,
|
|
183
|
+
});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
taken.set(id, `backing variable of output "${o.name}"`);
|
|
187
|
+
backing.set(o.name, id);
|
|
188
|
+
}
|
|
189
|
+
const reads = new Map([...backing].filter(([o]) => !use.stepNames.has(o)));
|
|
190
|
+
return { backing, reads, problems };
|
|
191
|
+
}
|
|
192
|
+
/** The `inout` global that backs output `o`: its type and value metadata, no description. */
|
|
193
|
+
export function outputBackingGlobal(o, id) {
|
|
194
|
+
return {
|
|
195
|
+
id, name: id, direction: 'inout', type: o.type,
|
|
196
|
+
...(o.default !== undefined ? { defaultValue: o.default } : {}),
|
|
197
|
+
...(o.subType !== undefined ? { subType: o.subType } : {}),
|
|
198
|
+
...(o.schema !== undefined ? { schema: o.schema } : {}),
|
|
199
|
+
};
|
|
200
|
+
}
|
package/dist/flow-sdk.d.ts
CHANGED
|
@@ -379,6 +379,11 @@ export interface NodeOptions {
|
|
|
379
379
|
* Flow JSON stores it. Keys name declared `.var()` variables (or flow
|
|
380
380
|
* outputs); values are `Expr`s or raw literals.
|
|
381
381
|
*
|
|
382
|
+
* A key that names a flow output writes the output's backing Variable,
|
|
383
|
+
* `<output>Value` (`direction: 'inout'`), because Studio Web's Update
|
|
384
|
+
* Variable cannot target an Output; every End then maps the output from that
|
|
385
|
+
* Variable.
|
|
386
|
+
*
|
|
382
387
|
* @remarks
|
|
383
388
|
* Not accepted on `.branch()`, `.switch()`, `.parallel()`, or `.trigger()` —
|
|
384
389
|
* the platform strips updates from decision/switch/merge nodes on save, and a
|
|
@@ -1439,6 +1444,16 @@ declare class FlowBuilder extends StepList {
|
|
|
1439
1444
|
* second variable (its default becomes the output's); a different type is a
|
|
1440
1445
|
* `check` error, `OUTPUT_VAR_MISMATCH`.
|
|
1441
1446
|
*
|
|
1447
|
+
* In the `.flow`, Studio Web sets an Output only on an End node, and neither
|
|
1448
|
+
* Update Variable nor an expression can touch one. So an output that the flow
|
|
1449
|
+
* sets or reads from inside (`{ updates }`, a `{ shared }` input, a
|
|
1450
|
+
* `default`, or a `v('<name>')` read) is emitted with a backing Variable,
|
|
1451
|
+
* `<name>Value` (`direction: 'inout'`): updates and reads go to the Variable,
|
|
1452
|
+
* and every End maps the output from it. That id must be free; a step,
|
|
1453
|
+
* input or `.var()` already called `<name>Value` is a `check` error,
|
|
1454
|
+
* `OUTPUT_BACKING_COLLISION`. An output only `.return({ … })` sets needs no
|
|
1455
|
+
* Variable: an End that does not name it maps `null`.
|
|
1456
|
+
*
|
|
1442
1457
|
* @example
|
|
1443
1458
|
* **A loop accumulator**
|
|
1444
1459
|
* ```ts
|
package/dist/flow-sdk.js
CHANGED
|
@@ -1176,6 +1176,16 @@ class FlowBuilder extends StepList {
|
|
|
1176
1176
|
* second variable (its default becomes the output's); a different type is a
|
|
1177
1177
|
* `check` error, `OUTPUT_VAR_MISMATCH`.
|
|
1178
1178
|
*
|
|
1179
|
+
* In the `.flow`, Studio Web sets an Output only on an End node, and neither
|
|
1180
|
+
* Update Variable nor an expression can touch one. So an output that the flow
|
|
1181
|
+
* sets or reads from inside (`{ updates }`, a `{ shared }` input, a
|
|
1182
|
+
* `default`, or a `v('<name>')` read) is emitted with a backing Variable,
|
|
1183
|
+
* `<name>Value` (`direction: 'inout'`): updates and reads go to the Variable,
|
|
1184
|
+
* and every End maps the output from it. That id must be free; a step,
|
|
1185
|
+
* input or `.var()` already called `<name>Value` is a `check` error,
|
|
1186
|
+
* `OUTPUT_BACKING_COLLISION`. An output only `.return({ … })` sets needs no
|
|
1187
|
+
* Variable: an End that does not name it maps `null`.
|
|
1188
|
+
*
|
|
1179
1189
|
* @example
|
|
1180
1190
|
* **A loop accumulator**
|
|
1181
1191
|
* ```ts
|
package/dist/flow-variables.d.ts
CHANGED
|
@@ -54,10 +54,12 @@ export interface FlowGlobalsPlan<D extends DeclaredVariable = DeclaredVariable>
|
|
|
54
54
|
* `@uipath/maestro-sdk` (the code that turns a `.flow` into what runs):
|
|
55
55
|
* - only an `out` global is a flow OUTPUT at the root — `buildNodeOutputMappings`
|
|
56
56
|
* drops an End node's mapping for any id that is not `direction: 'out'`;
|
|
57
|
-
* - `inout` is
|
|
58
|
-
* entry
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* - `inout` is not part of the caller's contract either way: `@uipath/flow-schema`
|
|
58
|
+
* packaging builds the entry-point input schema from `in` globals bound to the
|
|
59
|
+
* trigger, and the output schema from `out` globals only;
|
|
60
|
+
* - a loop body that reads and rewrites the value mid-run does so through the
|
|
61
|
+
* output's backing `inout` Variable (`flow-output-backing.ts`, #1092), because
|
|
62
|
+
* Studio Web neither updates nor reads an Output;
|
|
61
63
|
* - the `default` on `<uipath:output>` is serialized (`collectVariablesFromNodes`)
|
|
62
64
|
* and a subflow seeds its `out` globals from `defaultValue ?? null`
|
|
63
65
|
* (`buildSeedTask`). The v1 CLI's `variable add --direction out
|
package/dist/flow-variables.js
CHANGED
|
@@ -58,10 +58,12 @@ const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
|
58
58
|
* `@uipath/maestro-sdk` (the code that turns a `.flow` into what runs):
|
|
59
59
|
* - only an `out` global is a flow OUTPUT at the root — `buildNodeOutputMappings`
|
|
60
60
|
* drops an End node's mapping for any id that is not `direction: 'out'`;
|
|
61
|
-
* - `inout` is
|
|
62
|
-
* entry
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* - `inout` is not part of the caller's contract either way: `@uipath/flow-schema`
|
|
62
|
+
* packaging builds the entry-point input schema from `in` globals bound to the
|
|
63
|
+
* trigger, and the output schema from `out` globals only;
|
|
64
|
+
* - a loop body that reads and rewrites the value mid-run does so through the
|
|
65
|
+
* output's backing `inout` Variable (`flow-output-backing.ts`, #1092), because
|
|
66
|
+
* Studio Web neither updates nor reads an Output;
|
|
65
67
|
* - the `default` on `<uipath:output>` is serialized (`collectVariablesFromNodes`)
|
|
66
68
|
* and a subflow seeds its `out` globals from `defaultValue ?? null`
|
|
67
69
|
* (`buildSeedTask`). The v1 CLI's `variable add --direction out
|