@uipath/maestro-builder-sdk 6.20.5 → 6.21.0
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 +72 -72
- package/dist/api-members.md +143 -143
- package/dist/check.js +34 -160
- package/dist/core/actions.d.ts +0 -126
- package/dist/core/actions.js +0 -48
- package/dist/core/edit-distance.d.ts +8 -0
- package/dist/core/edit-distance.js +26 -0
- package/dist/core/js-code-spans.d.ts +24 -0
- package/dist/core/js-code-spans.js +148 -0
- package/dist/core/nodes/node-type.d.ts +17 -2
- package/dist/core/typed-raw-node.js +0 -2
- package/dist/decompile.js +107 -36
- package/dist/flow-node-registry.js +2 -1
- package/dist/flow-node-type.d.ts +6 -0
- package/dist/flow-output-backing.d.ts +53 -0
- package/dist/flow-output-backing.js +200 -0
- package/dist/flow-sdk.d.ts +16 -0
- package/dist/flow-sdk.js +11 -0
- package/dist/flow-variables.d.ts +6 -4
- package/dist/flow-variables.js +6 -4
- package/dist/nodes/queue/flow/check.d.ts +35 -0
- package/dist/nodes/queue/flow/check.js +126 -0
- package/dist/nodes/queue/flow/factories.d.ts +49 -0
- package/dist/nodes/queue/flow/factories.js +50 -0
- package/dist/nodes/queue/flow/family.d.ts +3 -0
- package/dist/nodes/queue/flow/family.js +8 -0
- package/dist/nodes/queue/flow/index.d.ts +3 -0
- package/dist/nodes/queue/flow/index.js +3 -0
- package/dist/nodes/queue/flow/node-type.d.ts +8 -0
- package/dist/nodes/queue/flow/node-type.js +265 -0
- package/dist/nodes/queue/flow/spec.d.ts +92 -0
- package/dist/nodes/queue/flow/spec.js +1 -0
- package/dist/serialize.js +70 -183
- package/package.json +2 -2
- /package/dist/{core/queue-record.d.ts → nodes/queue/flow/record.d.ts} +0 -0
- /package/dist/{core/queue-record.js → nodes/queue/flow/record.js} +0 -0
|
@@ -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
|
+
}
|
|
@@ -45,12 +45,26 @@ export interface OutputRead {
|
|
|
45
45
|
/** The step whose expression contains the read. */
|
|
46
46
|
reader: string;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* How the artifact words a read of a `fields` output: the code prefix its read
|
|
50
|
+
* diagnostics share (`<PREFIX>_READ_NOT_OUTPUT`, `_READ_WITHOUT_RETURNS`,
|
|
51
|
+
* `_READ_UNKNOWN_FIELD`, each in the family's catalog), how a message names the
|
|
52
|
+
* node (`'Queue item'`), and what to tell an author who declared no fields.
|
|
53
|
+
*/
|
|
54
|
+
export interface FieldsRead {
|
|
55
|
+
readonly codePrefix: string;
|
|
56
|
+
readonly label: string;
|
|
57
|
+
readonly whenUndeclared: string;
|
|
58
|
+
}
|
|
48
59
|
/**
|
|
49
60
|
* What reading a node's output yields at run time.
|
|
50
61
|
*
|
|
51
62
|
* `none` keeps the diagnostic code of the rule it replaces
|
|
52
63
|
* (`DATAFABRIC_DELETE_HAS_NO_OUTPUT`, `MOCK_HAS_NO_OUTPUT`, …) and builds the
|
|
53
|
-
* message itself, so a port changes no diagnostic an author sees.
|
|
64
|
+
* message itself, so a port changes no diagnostic an author sees. `fields`
|
|
65
|
+
* states the fields a read may name — `undefined` when the author declared none,
|
|
66
|
+
* which makes any field read its own mistake — and `read` keeps the wording of
|
|
67
|
+
* the read rule it replaces, for the same reason. `open` admits any field.
|
|
54
68
|
*/
|
|
55
69
|
export type OutputContract = {
|
|
56
70
|
readonly kind: 'none';
|
|
@@ -58,8 +72,9 @@ export type OutputContract = {
|
|
|
58
72
|
message(read: OutputRead): string;
|
|
59
73
|
} | {
|
|
60
74
|
readonly kind: 'fields';
|
|
61
|
-
readonly fields: Readonly<Record<string, TypeDesc
|
|
75
|
+
readonly fields: Readonly<Record<string, TypeDesc>> | undefined;
|
|
62
76
|
readonly open?: boolean;
|
|
77
|
+
readonly read: FieldsRead;
|
|
63
78
|
} | {
|
|
64
79
|
readonly kind: 'opaque';
|
|
65
80
|
};
|
|
@@ -23,8 +23,6 @@ import { ownEntry } from './own-entry.js';
|
|
|
23
23
|
export const TYPED_FACTORIES = {
|
|
24
24
|
'core.action.http': { factory: 'http({ managed: false })' },
|
|
25
25
|
'core.action.http.v2': { factory: 'http({ managed: true })' },
|
|
26
|
-
'core.action.queue.create': { factory: 'queueItem()' },
|
|
27
|
-
'core.action.queue.create-and-wait': { factory: 'queueItem({ wait: true })' },
|
|
28
26
|
'core.control.end': { factory: '.return()' },
|
|
29
27
|
'core.logic.decision': { factory: '.branch()' },
|
|
30
28
|
'core.logic.dowhile': { factory: '.doWhile(name, condition, body, { limit, breakEnabled })' },
|
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';
|
|
@@ -226,8 +228,6 @@ const T = {
|
|
|
226
228
|
hitlQuickForm: 'uipath.human-in-the-loop.quick-form',
|
|
227
229
|
hitlActionApp: 'uipath.human-in-the-loop.coded-action-app',
|
|
228
230
|
hitlDocValidation: 'uipath.human-in-the-loop.document-validation',
|
|
229
|
-
queueCreate: 'core.action.queue.create',
|
|
230
|
-
queueCreateAndWait: 'core.action.queue.create-and-wait',
|
|
231
231
|
summarize: 'uipath.pattern.deep-rag',
|
|
232
232
|
batchTransform: 'uipath.pattern.batch-transform',
|
|
233
233
|
sendMessage: 'uipath.conversational.send-message',
|
|
@@ -642,35 +642,6 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
|
|
|
642
642
|
case T.hitlActionApp:
|
|
643
643
|
case T.hitlDocValidation:
|
|
644
644
|
return emitHitl(node, imp, inputNames, graph);
|
|
645
|
-
case T.queueCreate:
|
|
646
|
-
case T.queueCreateAndWait: {
|
|
647
|
-
// `itemData` is the node's name for the surface's `item` (the reference
|
|
648
|
-
// documents the mapping); `queue` carries the three identity fields.
|
|
649
|
-
const q = (inputs.queue ?? {});
|
|
650
|
-
const rows = Object.entries((inputs.itemData ?? {}))
|
|
651
|
-
.map(([k, v]) => [k, emitVal(v, imp, inputNames)]);
|
|
652
|
-
const optStr = (key) => {
|
|
653
|
-
const raw = unwrapSource(inputs[key]);
|
|
654
|
-
// serialize writes '' for an omitted optional — that is not an authored
|
|
655
|
-
// value, so it must not come back as one.
|
|
656
|
-
if (raw === undefined || raw === '')
|
|
657
|
-
return undefined;
|
|
658
|
-
return raw.startsWith('=js:') ? emitBareExpr(raw.slice(4), imp, inputNames) : tsLiteral(raw);
|
|
659
|
-
};
|
|
660
|
-
const priority = unwrapPlain(inputs.priority);
|
|
661
|
-
return `${imp.use('queueItem')}(${opts([
|
|
662
|
-
['queue', tsText(q.name)],
|
|
663
|
-
['folderPath', tsText(q.folderPath)],
|
|
664
|
-
['key', tsText(q.key)],
|
|
665
|
-
['item', rows.length ? objLit(rows) : '{}'],
|
|
666
|
-
// `Normal` is the definition's default — only a departure was authored.
|
|
667
|
-
['priority', typeof priority === 'string' && priority !== 'Normal' ? tsLiteral(priority) : undefined],
|
|
668
|
-
['reference', optStr('reference')],
|
|
669
|
-
['deferDate', optStr('deferDate')],
|
|
670
|
-
['dueDate', optStr('dueDate')],
|
|
671
|
-
['wait', node.type === T.queueCreateAndWait ? 'true' : undefined],
|
|
672
|
-
])})`;
|
|
673
|
-
}
|
|
674
645
|
case T.summarize:
|
|
675
646
|
case T.batchTransform: {
|
|
676
647
|
const attachment = emitVal(inputs.attachment, imp, inputNames);
|
|
@@ -2342,6 +2313,7 @@ function armCb(param, segs) {
|
|
|
2342
2313
|
*/
|
|
2343
2314
|
function returnSeg(node, ctx) {
|
|
2344
2315
|
const updates = ctx.flow.variables?.variableUpdates?.[node.id] ?? [];
|
|
2316
|
+
const outputIds = new Set(readFlowGlobals(ctx.flow.variables).filter((g) => g.direction === 'out').map((g) => g.id));
|
|
2345
2317
|
const opts = returnOptsArg(node);
|
|
2346
2318
|
const entries = updates.map((u) => `${key(u.variableId)}: ${emitVal(u.expression, ctx.imp, ctx.inputNames)}`);
|
|
2347
2319
|
const written = new Set(updates.map((u) => u.variableId));
|
|
@@ -2350,7 +2322,7 @@ function returnSeg(node, ctx) {
|
|
|
2350
2322
|
continue;
|
|
2351
2323
|
const source = out?.source;
|
|
2352
2324
|
const raw = unwrapSource(source);
|
|
2353
|
-
if (raw === undefined || isIdentityEndSource(raw, name))
|
|
2325
|
+
if (raw === undefined || isIdentityEndSource(raw, name, outputIds.has(name)))
|
|
2354
2326
|
continue;
|
|
2355
2327
|
entries.push(`${key(name)}: ${emitVal(source, ctx.imp, ctx.inputNames)}`);
|
|
2356
2328
|
}
|
|
@@ -2358,10 +2330,15 @@ function returnSeg(node, ctx) {
|
|
|
2358
2330
|
return `.return({}${opts})`;
|
|
2359
2331
|
return `.return({ ${entries.join(', ')} }${opts})`;
|
|
2360
2332
|
}
|
|
2361
|
-
/**
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2333
|
+
/**
|
|
2334
|
+
* The fill-in mapping `endOutputs` writes for a variable no `.return` named:
|
|
2335
|
+
* `=js:$vars.<name>`, read through the backing Variable `foldOutputBacking`
|
|
2336
|
+
* has already folded back. An output with no backing Variable fills in as
|
|
2337
|
+
* `null` (#1092), since nothing but `.return()` sets it.
|
|
2338
|
+
*/
|
|
2339
|
+
function isIdentityEndSource(raw, name, isOutput = false) {
|
|
2340
|
+
const body = (raw.startsWith('=js:') ? raw.slice(4) : raw).trim();
|
|
2341
|
+
return body === `$vars.${name}` || (isOutput && body === 'null');
|
|
2365
2342
|
}
|
|
2366
2343
|
/**
|
|
2367
2344
|
* The options argument for a `.return(...)` — `version`, plus the End's `name`
|
|
@@ -3131,7 +3108,101 @@ function slugId(flow) {
|
|
|
3131
3108
|
const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
3132
3109
|
return slug || 'flow';
|
|
3133
3110
|
}
|
|
3111
|
+
// ─── Output backing Variables (#1092) ─────────────────────────────────────────
|
|
3112
|
+
/** Sorted-key JSON, so two globals compare equal whatever their key order. */
|
|
3113
|
+
function canonicalJson(v) {
|
|
3114
|
+
if (Array.isArray(v))
|
|
3115
|
+
return `[${v.map(canonicalJson).join(',')}]`;
|
|
3116
|
+
if (v && typeof v === 'object') {
|
|
3117
|
+
const o = v;
|
|
3118
|
+
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(',')}}`;
|
|
3119
|
+
}
|
|
3120
|
+
return JSON.stringify(v);
|
|
3121
|
+
}
|
|
3122
|
+
/**
|
|
3123
|
+
* The wire fields whose plain-string value is JavaScript (no `=js:` prefix):
|
|
3124
|
+
* a script body, a decision or switch `expression`, a loop's `condition` and
|
|
3125
|
+
* `completionCondition`, an HTTP branch's `conditionExpression`, a transform's
|
|
3126
|
+
* `collection`. `serialize` renames `$vars` reads in exactly these and in
|
|
3127
|
+
* `=js:` values.
|
|
3128
|
+
*/
|
|
3129
|
+
const RAW_JS_FIELDS = new Set(['script', 'expression', 'condition', 'completionCondition', 'conditionExpression', 'collection']);
|
|
3130
|
+
/**
|
|
3131
|
+
* Every `$vars.<from>` read (the whole root, not a prefix of a longer one) in
|
|
3132
|
+
* `v`'s code, read as `$vars.<to>`. Code is a `=js:` wire value, a plain
|
|
3133
|
+
* `$vars.` path, or a `RAW_JS_FIELDS` value; in each, only the code spans are
|
|
3134
|
+
* rewritten. A label, a literal input, or a quoted `"$vars.<from>"` inside a
|
|
3135
|
+
* script stays as written, the way `serialize` left it (#1092 review).
|
|
3136
|
+
*/
|
|
3137
|
+
function renameVarRoot(v, from, to) {
|
|
3138
|
+
const re = new RegExp(`\\$vars\\.${from.replace(/[$]/g, '\\$&')}(?![A-Za-z0-9_$])`, 'g');
|
|
3139
|
+
const inCode = (js) => mapJsCode(js, (code) => code.replace(re, `$vars.${to}`));
|
|
3140
|
+
const walk = (x, key) => {
|
|
3141
|
+
if (typeof x === 'string') {
|
|
3142
|
+
if (x.startsWith('=js:'))
|
|
3143
|
+
return '=js:' + inCode(x.slice(4));
|
|
3144
|
+
if (x.startsWith('$vars.') || (key !== undefined && RAW_JS_FIELDS.has(key)))
|
|
3145
|
+
return inCode(x);
|
|
3146
|
+
return x;
|
|
3147
|
+
}
|
|
3148
|
+
if (Array.isArray(x))
|
|
3149
|
+
return x.map((y) => walk(y, key));
|
|
3150
|
+
if (x && typeof x === 'object')
|
|
3151
|
+
return Object.fromEntries(Object.entries(x).map(([k, y]) => [k, walk(y, k)]));
|
|
3152
|
+
return x;
|
|
3153
|
+
};
|
|
3154
|
+
return walk(v);
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Undo the backing-Variable lowering `serialize` applies to an output the flow
|
|
3158
|
+
* sets from inside (`flow-output-backing.ts`): fold the `inout` `<output>Value`
|
|
3159
|
+
* global back into its output, so the source says `{ updates: { summary } }`
|
|
3160
|
+
* and `v('summary')` again and recompiling emits the same bytes.
|
|
3161
|
+
*
|
|
3162
|
+
* A pair is folded only when the Variable is exactly what `serialize` would
|
|
3163
|
+
* emit for that output (`outputBackingGlobal`): same id, type, default,
|
|
3164
|
+
* subType and schema, no description, no trigger. Any other `inout` global,
|
|
3165
|
+
* one a person named `summaryValue` with a description say, stays a `.var()`.
|
|
3166
|
+
* Applied to each subflow body too, against that body's own globals.
|
|
3167
|
+
*/
|
|
3168
|
+
function foldOutputBacking(flow) {
|
|
3169
|
+
const subflows = flow.subflows
|
|
3170
|
+
? Object.fromEntries(Object.entries(flow.subflows).map(([k, c]) => [k, foldOutputBacking(c)]))
|
|
3171
|
+
: undefined;
|
|
3172
|
+
let out = subflows ? { ...flow, subflows } : flow;
|
|
3173
|
+
const globals = Array.isArray(flow.variables?.globals) ? flow.variables.globals : [];
|
|
3174
|
+
for (const o of globals.filter((g) => g.direction === 'out')) {
|
|
3175
|
+
const id = outputBackingName(o.id);
|
|
3176
|
+
const b = globals.find((g) => g.id === id && g.direction === 'inout');
|
|
3177
|
+
if (!b)
|
|
3178
|
+
continue;
|
|
3179
|
+
const expected = outputBackingGlobal({ name: o.id, type: o.type, default: o.defaultValue, subType: o.subType, schema: o.schema }, id);
|
|
3180
|
+
if (canonicalJson(b) !== canonicalJson(expected))
|
|
3181
|
+
continue;
|
|
3182
|
+
const vars = out.variables;
|
|
3183
|
+
const updates = Object.fromEntries(Object.entries(vars.variableUpdates ?? {}).map(([node, rows]) => [
|
|
3184
|
+
node, rows.map((r) => (r.variableId === id ? { ...r, variableId: o.id } : r)),
|
|
3185
|
+
]));
|
|
3186
|
+
const nodes = (out.nodes ?? []).map((n) => {
|
|
3187
|
+
if (n.type !== 'core.control.end' || !n.outputs || !(id in n.outputs))
|
|
3188
|
+
return n;
|
|
3189
|
+
const { [id]: _dropped, ...rest } = n.outputs;
|
|
3190
|
+
return { ...n, outputs: rest };
|
|
3191
|
+
});
|
|
3192
|
+
out = {
|
|
3193
|
+
...out,
|
|
3194
|
+
nodes: renameVarRoot(nodes, id, o.id),
|
|
3195
|
+
variables: {
|
|
3196
|
+
...vars,
|
|
3197
|
+
globals: (vars.globals ?? []).filter((g) => g !== b && g.id !== id),
|
|
3198
|
+
variableUpdates: renameVarRoot(updates, id, o.id),
|
|
3199
|
+
},
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
return out;
|
|
3203
|
+
}
|
|
3134
3204
|
export function decompile(flow, options = {}) {
|
|
3205
|
+
flow = foldOutputBacking(flow);
|
|
3135
3206
|
const importSpecifier = options.importSpecifier ?? './flow-sdk.js';
|
|
3136
3207
|
runStrict = options.bestEffort === false;
|
|
3137
3208
|
runWarn = options.warn;
|
|
@@ -12,8 +12,9 @@ import { dataFabricFlowFamily } from './nodes/data-fabric/flow/family.js';
|
|
|
12
12
|
import { delayFlowFamily } from './nodes/delay/flow/family.js';
|
|
13
13
|
import { documentFlowFamily } from './nodes/document/flow/family.js';
|
|
14
14
|
import { mockFlowFamily } from './nodes/mock/flow/family.js';
|
|
15
|
+
import { queueFlowFamily } from './nodes/queue/flow/family.js';
|
|
15
16
|
import { scriptFlowFamily } from './nodes/script/flow/family.js';
|
|
16
17
|
import { transformFlowFamily } from './nodes/transform/flow/family.js';
|
|
17
18
|
/** The families, in no significant order. Adding one is the whole of registering it. */
|
|
18
|
-
export const FLOW_NODE_FAMILIES = [dataFabricFlowFamily, delayFlowFamily, documentFlowFamily, mockFlowFamily, scriptFlowFamily, transformFlowFamily];
|
|
19
|
+
export const FLOW_NODE_FAMILIES = [dataFabricFlowFamily, delayFlowFamily, documentFlowFamily, mockFlowFamily, queueFlowFamily, scriptFlowFamily, transformFlowFamily];
|
|
19
20
|
export const FLOW_NODE_TYPES = new NodeTypeRegistry('flow', FLOW_NODE_FAMILIES, { hasBundledDefinition });
|
package/dist/flow-node-type.d.ts
CHANGED
|
@@ -75,6 +75,12 @@ export interface FlowSerializeContext {
|
|
|
75
75
|
} & Readonly<Record<string, unknown>>): void;
|
|
76
76
|
/** The bundled definition `<typeName>@<version>`, or undefined when the SDK does not ship it. */
|
|
77
77
|
definition(typeName: string, version: string): unknown;
|
|
78
|
+
/**
|
|
79
|
+
* The flow's bindings so far, by id — read-only. For a type that SHARES a
|
|
80
|
+
* binding with earlier steps on the same resource (a queue's name + folder
|
|
81
|
+
* pair) or must pick an id that is not taken; add entries through `bind`.
|
|
82
|
+
*/
|
|
83
|
+
readonly bindings: ReadonlyMap<string, Readonly<Record<string, unknown>>>;
|
|
78
84
|
}
|
|
79
85
|
/** What `deserialize` may ask of the decompiler, for one serialized node. */
|
|
80
86
|
export interface FlowDeserializeContext {
|
|
@@ -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
|
+
}
|