@uipath/maestro-builder-sdk 6.16.7 → 6.16.9
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 +6 -6
- package/dist/case/compile-cli.js +2 -7
- package/dist/check.js +10 -0
- package/dist/core/expr-check.d.ts +32 -1
- package/dist/core/expr-check.js +125 -7
- package/dist/core/expr.d.ts +51 -0
- package/dist/core/expr.js +157 -4
- package/dist/core/stable-id.d.ts +33 -0
- package/dist/core/stable-id.js +56 -0
- package/dist/flow-expr-check.d.ts +17 -0
- package/dist/flow-expr-check.js +73 -12
- package/dist/serialize.js +11 -42
- package/package.json +2 -2
package/dist/api-index.md
CHANGED
|
@@ -166,7 +166,7 @@ could hand you spans for a version you do not have.
|
|
|
166
166
|
| `ixpExtract` | function | `dist/core/actions.d.ts:2670-2724` | Actions | Extract fields from a document with a published IxP project — the platform's **Extract** node (`uipath.ixp.*`, service… |
|
|
167
167
|
| `IxpExtractInputs` | interface | `dist/core/actions.d.ts:879-953` | Option shapes | 9 field(s) |
|
|
168
168
|
| `IxpToolRef` | interface | `dist/core/actions.d.ts:1891-1929` | Supporting types | A published IxP (Intelligent eXtraction Platform) project as a tool — the agent decides when to extract a document and… |
|
|
169
|
-
| `js` | function | `dist/core/expr.d.ts:241-
|
|
169
|
+
| `js` | function | `dist/core/expr.d.ts:241-276` | Expressions and types | Build a raw JS *expression* (a condition or computation). |
|
|
170
170
|
| `lit` | function | `dist/core/expr.d.ts:22-28` | Expressions and types | A constant value baked directly into a node input. |
|
|
171
171
|
| `lookup` | function | `dist/core/lookups.d.ts:95-102` | Generated descriptors | Begin resolving a lookup field on a generated descriptor. |
|
|
172
172
|
| `lookup` | function | `dist/core/lookups.d.ts:103-122` | Generated descriptors | Begin resolving a lookup field addressed by connector key and action. |
|
|
@@ -232,7 +232,7 @@ could hand you spans for a version you do not have.
|
|
|
232
232
|
| `summarize` | function | `dist/core/actions.d.ts:2594-2634` | Actions | Summarize a document with citations — the platform's **Summarize** node. |
|
|
233
233
|
| `SummarizeInputs` | interface | `dist/core/actions.d.ts:806-838` | Option shapes | 3 field(s) |
|
|
234
234
|
| `SwitchArm` | interface | `dist/flow-sdk.d.ts:329-334` | Supporting types | One arm of a built `.switch` (the serializer's view: body already collected). |
|
|
235
|
-
| `tmpl` | function | `dist/core/expr.d.ts:
|
|
235
|
+
| `tmpl` | function | `dist/core/expr.d.ts:277-287` | Expressions and types | Build a JS *string template* (for URLs, messages, etc.). |
|
|
236
236
|
| `ToolRef` | type | `dist/core/actions.d.ts:1657-1666` | Supporting types | One tool on an inline agent — a discriminated union over the kinds the tenant's registry actually serves… |
|
|
237
237
|
| `transform` | function | `dist/core/actions.d.ts:2496-2541` | Actions | Declare a Transform action — a chain of declarative operations over a collection. |
|
|
238
238
|
| `Transformation` | type | `dist/core/actions.d.ts:248-249` | Supporting types | What a map does to a field's value. |
|
|
@@ -244,8 +244,8 @@ could hand you spans for a version you do not have.
|
|
|
244
244
|
| `TriggerMeta` | interface | `dist/core/connectors.d.ts:120-129` | Generated descriptors | Runtime metadata a generated trigger descriptor carries. |
|
|
245
245
|
| `TriggerOptions` | interface | `dist/core/actions.d.ts:3410-3457` | Option shapes | The typed `onEvent`/`waitForEvent` options — everything an EventSubscription carries except `connector`/`event`, which… |
|
|
246
246
|
| `TriggerSpec` | type | `dist/flow-sdk.d.ts:150-168` | Supporting types | What starts the flow. |
|
|
247
|
-
| `TypeDesc` | type | `dist/core/expr.d.ts:
|
|
248
|
-
| `types` | const | `dist/core/expr.d.ts:
|
|
247
|
+
| `TypeDesc` | type | `dist/core/expr.d.ts:379` | Supporting types | |
|
|
248
|
+
| `types` | const | `dist/core/expr.d.ts:363-378` | Expressions and types | Variable type descriptors (map to Flow variable `type` values). |
|
|
249
249
|
| `unresolvedLookupMessage` | function | `dist/core/lookups.d.ts:180-189` | Generated descriptors | The message for a lookup nothing has resolved — the command that fixes it. |
|
|
250
250
|
| `v` | function | `dist/core/expr.d.ts:29-41` | Expressions and types | Reference a flow variable or output by name → `$vars.<name>`. |
|
|
251
251
|
| `VarDecl` | interface | `dist/flow-sdk.d.ts:47-59` | Supporting types | 7 field(s) |
|
|
@@ -337,7 +337,7 @@ could hand you spans for a version you do not have.
|
|
|
337
337
|
| `TriggerDescriptor` | type | `dist/core/connectors.d.ts:130-138` | Supporting types | A generated, typed connector-trigger descriptor: TriggerMeta branded with phantom `where`/output types. |
|
|
338
338
|
| `TriggerMeta` | interface | `dist/core/connectors.d.ts:120-129` | Supporting types | Runtime metadata a generated trigger descriptor carries. |
|
|
339
339
|
| `TriggerOptions` | interface | `dist/core/actions.d.ts:3410-3457` | Option shapes | The typed `onEvent`/`waitForEvent` options — everything an EventSubscription carries except `connector`/`event`, which… |
|
|
340
|
-
| `TypeDesc` | type | `dist/core/expr.d.ts:
|
|
340
|
+
| `TypeDesc` | type | `dist/core/expr.d.ts:379` | Supporting types | |
|
|
341
341
|
| `UnresolvedReferenceTaskKind` | type | `dist/case/case-sdk.d.ts:358-359` | Supporting types | Published-resource task kinds that the Case schema permits as unresolved skeletons. |
|
|
342
342
|
| `WaitConnectorPlaceholderSpec` | interface | `dist/case/case-sdk.d.ts:360-370` | Supporting types | A `wait-for-connector` subscription: suspend on an Integration Service event. |
|
|
343
343
|
| `WaitConnectorSpec` | type | `dist/case/case-sdk.d.ts:371-375` | Supporting types | A placeholder connector/operation pair, or a library-resolved event subscription using the same symbolic shape as Flow… |
|
|
@@ -436,7 +436,7 @@ could hand you spans for a version you do not have.
|
|
|
436
436
|
| `TimerLike` | type | `dist/bpmn/bpmn-sdk.d.ts:40-41` | Supporting types | A timer as an ISO-8601 duration string (shorthand for `{ duration }`) or a full spec. |
|
|
437
437
|
| `TimerSpec` | interface | `dist/bpmn/bpmn-sdk.d.ts:31-39` | Supporting types | ISO-8601 timer specification (one of duration / date / cycle). |
|
|
438
438
|
| `TypedContextRow` | interface | `dist/bpmn/typed-node.d.ts:155-186` | Supporting types | One `uipath:context` input row, spelled out. |
|
|
439
|
-
| `TypeDesc` | type | `dist/core/expr.d.ts:
|
|
439
|
+
| `TypeDesc` | type | `dist/core/expr.d.ts:379` | Supporting types | |
|
|
440
440
|
| `TypedInputRow` | interface | `dist/bpmn/typed-node.d.ts:187-209` | Supporting types | One `uipath:input` payload row spelled out — the `separateInputs` counterpart of TypedContextRow. |
|
|
441
441
|
| `TypedOutputRow` | interface | `dist/bpmn/typed-node.d.ts:246-285` | Supporting types | 9 field(s) |
|
|
442
442
|
| `VarDirection` | type | `dist/bpmn/bpmn-sdk.d.ts:361-365` | Supporting types | How a variable is exposed: `'input'` (read-only entry input), `'output'` (a return value), or `'inputOutput'` (a mutable… |
|
package/dist/case/compile-cli.js
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
import { reportResult } from '../cli-result.js';
|
|
13
13
|
import { writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
14
14
|
import { join, dirname, basename, resolve } from 'node:path';
|
|
15
|
-
import { createHash } from 'node:crypto';
|
|
16
15
|
import { resolveCaseFile, loadBuiltCase } from './load.js';
|
|
17
16
|
import { serialize } from './serialize.js';
|
|
18
17
|
import { check } from './check.js';
|
|
@@ -23,6 +22,7 @@ import { defaultBindingsFile } from '../workdir.js';
|
|
|
23
22
|
import { flagValue, runWhenInvokedDirectly } from '../cli-run.js';
|
|
24
23
|
import { ensureTypeScriptRuntime } from '../node-runtime.js';
|
|
25
24
|
import { CASE_FORMAT_PROFILE } from './format-profile.js';
|
|
25
|
+
import { sha256Uuid } from '../core/stable-id.js';
|
|
26
26
|
export async function run(argv) {
|
|
27
27
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
28
28
|
console.error('usage: compile <case.ts | BaseName> [-o caseplan.json]');
|
|
@@ -128,7 +128,7 @@ function syncEntryPoints(caseplan, outPath) {
|
|
|
128
128
|
const { input, output } = entryPointIO(caseplan.variables, n.id);
|
|
129
129
|
return {
|
|
130
130
|
filePath: `/content/${base}.bpmn#${n.id}`,
|
|
131
|
-
uniqueId: prev?.uniqueId ??
|
|
131
|
+
uniqueId: prev?.uniqueId ?? sha256Uuid(`entrypoint:${n.id}`),
|
|
132
132
|
type: 'CaseManagement',
|
|
133
133
|
input,
|
|
134
134
|
output,
|
|
@@ -279,9 +279,4 @@ function isRecord(value) {
|
|
|
279
279
|
function stringValue(value) {
|
|
280
280
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
281
281
|
}
|
|
282
|
-
/** A deterministic UUID-shaped id (not a real v4 — the schema only needs a stable string). */
|
|
283
|
-
function stableUuid(seed) {
|
|
284
|
-
const h = createHash('sha256').update(seed).digest('hex');
|
|
285
|
-
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
286
|
-
}
|
|
287
282
|
runWhenInvokedDirectly(import.meta.url, 'compile', run);
|
package/dist/check.js
CHANGED
|
@@ -9,6 +9,7 @@ import { Expr, SCHEDULE_PRESETS, DELAY_PRESETS, VARIANT_OPERATION, parseIxpProje
|
|
|
9
9
|
import { FlowNode } from './core/node-classes.js';
|
|
10
10
|
import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
|
|
11
11
|
import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
|
|
12
|
+
import { checkCookedEscapes } from './flow-expr-check.js';
|
|
12
13
|
import { FLOW_EXPR_DIALECT, checkBindingLabels, checkBindingPresence, checkConnectorContract as coreCheckConnectorContract, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, collectConnectionUses, connectionConnectorConflicts, } from './core/connector-checks.js';
|
|
13
14
|
import { prepareCommand } from './core/cli-spelling.js';
|
|
14
15
|
import { editDistance } from './core/edit-distance.js';
|
|
@@ -25,6 +26,15 @@ export function check(built, opts = {}) {
|
|
|
25
26
|
for (const { id, message } of connectionConnectorConflicts(uses, opts)) {
|
|
26
27
|
diags.push({ level: 'error', code: 'CONNECTION_CONNECTOR_CONFLICT', step: id, message });
|
|
27
28
|
}
|
|
29
|
+
// Once, from the top: the walk reaches child flows itself, so running it per
|
|
30
|
+
// `checkFlow` would report a child's escape twice.
|
|
31
|
+
for (const d of checkCookedEscapes(built)) {
|
|
32
|
+
diags.push({
|
|
33
|
+
level: d.level, code: d.code, message: `In ${d.where}: ${d.message}`,
|
|
34
|
+
...(d.step !== undefined ? { step: d.step } : {}),
|
|
35
|
+
...(d.suggestion ? { suggestion: d.suggestion } : {}),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
28
38
|
return diags;
|
|
29
39
|
}
|
|
30
40
|
/**
|
|
@@ -29,6 +29,37 @@
|
|
|
29
29
|
* and gets back diagnostics with no location. The caller stamps the location
|
|
30
30
|
* (which node / task / field) it came from.
|
|
31
31
|
*/
|
|
32
|
+
import type { CookedEscape } from './expr.js';
|
|
33
|
+
/** What one cooked escape did to an expression. */
|
|
34
|
+
export interface CookedEscapeEffect {
|
|
35
|
+
/** The escape as written, e.g. `\d`. */
|
|
36
|
+
escape: string;
|
|
37
|
+
/** What TypeScript turned it into. */
|
|
38
|
+
cooked: string;
|
|
39
|
+
/** `meaning`: the expression parses but means something else. `syntax`: it no longer parses. */
|
|
40
|
+
effect: 'meaning' | 'syntax';
|
|
41
|
+
/** The escape was invalid, so `js` wrote `undefined` for its whole part. */
|
|
42
|
+
invalid?: true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Which of an expression's cooked escapes changed it, and how. Harmless ones are
|
|
46
|
+
* left out. See the block comment above for the rule.
|
|
47
|
+
*/
|
|
48
|
+
export declare function cookedEscapeEffects(expr: string, escapes: readonly CookedEscape[]): CookedEscapeEffect[];
|
|
49
|
+
/** The escape spelled so it survives: one more backslash. */
|
|
50
|
+
export declare function keptEscape(escape: string): string;
|
|
51
|
+
/** The sentence that explains one cooked escape, shared by the warning and EXPR_SYNTAX. */
|
|
52
|
+
export declare function cookedEscapeSentence(e: CookedEscapeEffect): string;
|
|
53
|
+
/**
|
|
54
|
+
* A diagnostic per escape that changed what an expression means. Location is
|
|
55
|
+
* stamped by the caller.
|
|
56
|
+
*
|
|
57
|
+
* * JS_ESCAPE_COOKED (warning) — a valid escape cooked to another character:
|
|
58
|
+
* `/\d+/` became `/d+/`. The expression may still be what the author wants.
|
|
59
|
+
* * JS_ESCAPE_INVALID (error) — an invalid escape (`\1`, `\x4`): the whole part
|
|
60
|
+
* of the template became the word `undefined`, which is never what was meant.
|
|
61
|
+
*/
|
|
62
|
+
export declare function cookedEscapeDiagnostics(expr: string, escapes: readonly CookedEscape[]): ExprDiagnostic[];
|
|
32
63
|
/** A `$vars.*` reference found in an expression. */
|
|
33
64
|
export interface ExprRef {
|
|
34
65
|
/** The first segment after `$vars.` — the name the reference is rooted at. */
|
|
@@ -90,4 +121,4 @@ export declare function extractRefs(expr: string, namespace?: string): ExprRef[]
|
|
|
90
121
|
* message). An empty result means "nothing this check is sure is wrong" — never
|
|
91
122
|
* "fully valid"; the `validate` CLI is what proves the rest.
|
|
92
123
|
*/
|
|
93
|
-
export declare function checkExpression(expr: string, scope: ExprScope): ExprDiagnostic[];
|
|
124
|
+
export declare function checkExpression(expr: string, scope: ExprScope, escapes?: readonly CookedEscape[]): ExprDiagnostic[];
|
package/dist/core/expr-check.js
CHANGED
|
@@ -50,16 +50,129 @@ const JSONPATH_WILDCARD = /\[\s*(?:\*|\?|'[^']*'|"[^"]*")\s*\]|\.\.[A-Za-z_$]/;
|
|
|
50
50
|
* this adds no dependency.
|
|
51
51
|
*/
|
|
52
52
|
function syntaxError(expr) {
|
|
53
|
+
const problems = parseExpression(expr).problems;
|
|
54
|
+
if (problems.length === 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
return ts.flattenDiagnosticMessageText(problems[0].messageText, ' ');
|
|
57
|
+
}
|
|
58
|
+
function parseExpression(expr) {
|
|
53
59
|
const source = ts.createSourceFile('expression.ts', `(${expr}\n)`, ts.ScriptTarget.ES2020,
|
|
54
60
|
/* setParentNodes */ false);
|
|
55
61
|
// `parseDiagnostics` is where the scanner records syntax problems. It is not on
|
|
56
62
|
// the public `SourceFile` type, hence the cast; there is no public API that
|
|
57
63
|
// parses a fragment and hands back its syntax diagnostics without also running
|
|
58
64
|
// a full program's type-check.
|
|
59
|
-
const problems = source.parseDiagnostics;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
const problems = source.parseDiagnostics ?? [];
|
|
66
|
+
return { source, problems };
|
|
67
|
+
}
|
|
68
|
+
/** Replace each chosen escape's cooked text with what was typed. Escapes are disjoint and in order. */
|
|
69
|
+
function retype(expr, escapes, keepCooked) {
|
|
70
|
+
let out = '';
|
|
71
|
+
let pos = 0;
|
|
72
|
+
for (const e of escapes) {
|
|
73
|
+
out += expr.slice(pos, e.at) + (e === keepCooked ? e.cooked : e.typed);
|
|
74
|
+
pos = e.at + e.cooked.length;
|
|
75
|
+
}
|
|
76
|
+
return out + expr.slice(pos);
|
|
77
|
+
}
|
|
78
|
+
/** The value a leaf denotes — a name, or a literal's value (a regular expression's is its source). */
|
|
79
|
+
function leafValue(node) {
|
|
80
|
+
if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node))
|
|
81
|
+
return node.text;
|
|
82
|
+
// NumericLiteral … TemplateTail: every literal and template piece carries `.text`.
|
|
83
|
+
if (node.kind >= ts.SyntaxKind.FirstLiteralToken && node.kind <= ts.SyntaxKind.LastTemplateToken) {
|
|
84
|
+
return node.text;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function children(node) {
|
|
89
|
+
const out = [];
|
|
90
|
+
ts.forEachChild(node, (c) => { out.push(c); });
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function sameTree(a, b) {
|
|
94
|
+
if (a.kind !== b.kind || leafValue(a) !== leafValue(b))
|
|
95
|
+
return false;
|
|
96
|
+
const ca = children(a);
|
|
97
|
+
const cb = children(b);
|
|
98
|
+
return ca.length === cb.length && ca.every((c, i) => sameTree(c, cb[i]));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Which of an expression's cooked escapes changed it, and how. Harmless ones are
|
|
102
|
+
* left out. See the block comment above for the rule.
|
|
103
|
+
*/
|
|
104
|
+
export function cookedEscapeEffects(expr, escapes) {
|
|
105
|
+
if (escapes.length === 0)
|
|
106
|
+
return [];
|
|
107
|
+
const sorted = [...escapes].sort((a, b) => a.at - b.at);
|
|
108
|
+
const typed = parseExpression(retype(expr, sorted));
|
|
109
|
+
if (typed.problems.length)
|
|
110
|
+
return [];
|
|
111
|
+
const effects = [];
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
for (const e of sorted) {
|
|
114
|
+
const one = parseExpression(retype(expr, sorted, e));
|
|
115
|
+
const effect = one.problems.length ? 'syntax' : sameTree(typed.source, one.source) ? undefined : 'meaning';
|
|
116
|
+
if (!effect || seen.has(`${effect} ${e.escape}`))
|
|
117
|
+
continue;
|
|
118
|
+
seen.add(`${effect} ${e.escape}`);
|
|
119
|
+
effects.push({ escape: e.escape, cooked: e.cooked, effect, ...(e.invalid ? { invalid: true } : {}) });
|
|
120
|
+
}
|
|
121
|
+
return effects;
|
|
122
|
+
}
|
|
123
|
+
const CHARACTER_NAMES = {
|
|
124
|
+
'\b': 'backspace', '\f': 'form feed', '\n': 'line feed', '\r': 'carriage return',
|
|
125
|
+
'\t': 'tab', '\v': 'vertical tab', '\0': 'NUL', '\u2028': 'line separator', '\u2029': 'paragraph separator',
|
|
126
|
+
};
|
|
127
|
+
/** How to say what an escape became, in a message. */
|
|
128
|
+
function describeCooked(cooked) {
|
|
129
|
+
if (cooked === '')
|
|
130
|
+
return 'nothing (a line continuation)';
|
|
131
|
+
const name = CHARACTER_NAMES[cooked];
|
|
132
|
+
if (name)
|
|
133
|
+
return `U+${cooked.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')} (${name})`;
|
|
134
|
+
return `\`${cooked}\``;
|
|
135
|
+
}
|
|
136
|
+
/** The escape spelled so it survives: one more backslash. */
|
|
137
|
+
export function keptEscape(escape) {
|
|
138
|
+
return '\\' + escape;
|
|
139
|
+
}
|
|
140
|
+
/** The sentence that explains one cooked escape, shared by the warning and EXPR_SYNTAX. */
|
|
141
|
+
export function cookedEscapeSentence(e) {
|
|
142
|
+
const fix = `write \`${keptEscape(e.escape)}\` to keep the backslash.`;
|
|
143
|
+
if (e.invalid) {
|
|
144
|
+
return `\`${e.escape}\` is not a valid escape in a js\`\` template, so TypeScript drops that whole part of `
|
|
145
|
+
+ `the template and the expression holds the word \`undefined\` in its place; ${fix}`;
|
|
146
|
+
}
|
|
147
|
+
return `\`${e.escape}\` in a js\`\` template is read by TypeScript as ${describeCooked(e.cooked)} before the `
|
|
148
|
+
+ `SDK sees it; ${fix}`;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A diagnostic per escape that changed what an expression means. Location is
|
|
152
|
+
* stamped by the caller.
|
|
153
|
+
*
|
|
154
|
+
* * JS_ESCAPE_COOKED (warning) — a valid escape cooked to another character:
|
|
155
|
+
* `/\d+/` became `/d+/`. The expression may still be what the author wants.
|
|
156
|
+
* * JS_ESCAPE_INVALID (error) — an invalid escape (`\1`, `\x4`): the whole part
|
|
157
|
+
* of the template became the word `undefined`, which is never what was meant.
|
|
158
|
+
*/
|
|
159
|
+
export function cookedEscapeDiagnostics(expr, escapes) {
|
|
160
|
+
return cookedEscapeEffects(expr, escapes)
|
|
161
|
+
.filter((e) => e.effect === 'meaning')
|
|
162
|
+
.map((e) => e.invalid
|
|
163
|
+
? {
|
|
164
|
+
level: 'error',
|
|
165
|
+
code: 'JS_ESCAPE_INVALID',
|
|
166
|
+
message: cookedEscapeSentence(e),
|
|
167
|
+
suggestion: keptEscape(e.escape),
|
|
168
|
+
}
|
|
169
|
+
: {
|
|
170
|
+
level: 'warning',
|
|
171
|
+
code: 'JS_ESCAPE_COOKED',
|
|
172
|
+
message: `${cookedEscapeSentence(e)} The expression parses, so nothing else reports it, but it no longer `
|
|
173
|
+
+ `means what was typed.`,
|
|
174
|
+
suggestion: keptEscape(e.escape),
|
|
175
|
+
});
|
|
63
176
|
}
|
|
64
177
|
const DEFAULT_NAMESPACE = '$vars';
|
|
65
178
|
/**
|
|
@@ -149,7 +262,7 @@ function nearMiss(name, roots) {
|
|
|
149
262
|
* message). An empty result means "nothing this check is sure is wrong" — never
|
|
150
263
|
* "fully valid"; the `validate` CLI is what proves the rest.
|
|
151
264
|
*/
|
|
152
|
-
export function checkExpression(expr, scope) {
|
|
265
|
+
export function checkExpression(expr, scope, escapes = []) {
|
|
153
266
|
const namespace = scope.namespace ?? DEFAULT_NAMESPACE;
|
|
154
267
|
const noun = scope.noun ?? 'input, variable, or step';
|
|
155
268
|
const diags = [];
|
|
@@ -158,11 +271,16 @@ export function checkExpression(expr, scope) {
|
|
|
158
271
|
// references, and reporting both would just be noise on one root cause.
|
|
159
272
|
const syntax = scope.syntax === 'javascript' ? syntaxError(expr) : undefined;
|
|
160
273
|
if (syntax) {
|
|
274
|
+
// A `js` template whose escape broke the parse: say which one, or the author
|
|
275
|
+
// goes looking for an unterminated string they never wrote.
|
|
276
|
+
const cause = cookedEscapeEffects(expr, escapes).filter((e) => e.effect === 'syntax');
|
|
161
277
|
return [{
|
|
162
278
|
level: 'error',
|
|
163
279
|
code: 'EXPR_SYNTAX',
|
|
164
|
-
message: `Expression is not valid JavaScript: ${syntax}
|
|
165
|
-
|
|
280
|
+
message: `Expression is not valid JavaScript: ${syntax}`
|
|
281
|
+
+ cause.map((e) => ` ${cookedEscapeSentence(e)}`).join(''),
|
|
282
|
+
...(cause.length ? { suggestion: keptEscape(cause[0].escape) } : {}),
|
|
283
|
+
...(!cause.length && JSONPATH_WILDCARD.test(expr)
|
|
166
284
|
? {
|
|
167
285
|
suggestion: 'This looks like JSONPath. The runtime evaluates JavaScript, so index or map '
|
|
168
286
|
+ 'explicitly — `…issues` for the whole array, `…issues.map(i => i.key)` for one field.',
|
package/dist/core/expr.d.ts
CHANGED
|
@@ -243,11 +243,35 @@ export declare function err(step: string, field?: ErrorEnvelopeField): Expr;
|
|
|
243
243
|
* Interpolated `Expr`s contribute their reference; other values are JSON-encoded.
|
|
244
244
|
* e.g. js`${input('from')} !== ${input('to')}` → `$vars.from !== $vars.to`
|
|
245
245
|
*
|
|
246
|
+
* @remarks
|
|
247
|
+
* **Backslashes are read by TypeScript first.** The expression is the template's
|
|
248
|
+
* COOKED text: TypeScript applies every backslash escape before `js` sees it, the
|
|
249
|
+
* same as in any template literal. So a regular expression typed with one
|
|
250
|
+
* backslash loses it — `` js`/\d+/.test(${x})` `` emits `/d+/.test(…)`, which
|
|
251
|
+
* matches the letter `d`, and `\b` becomes the backspace character, not a word
|
|
252
|
+
* boundary. Write the backslash twice to keep it: `` js`/\\d+/.test(${x})` ``
|
|
253
|
+
* emits `/\d+/.test(…)`. Likewise `` \` `` is a backtick, and `\${` keeps `${` from
|
|
254
|
+
* starting an interpolation.
|
|
255
|
+
* This is the spelling `decompile` writes.
|
|
256
|
+
*
|
|
257
|
+
* `check` reports an escape that changed what the expression means
|
|
258
|
+
* (JS_ESCAPE_COOKED), and an `EXPR_SYNTAX` error names the escape when one broke
|
|
259
|
+
* the parse (`'\n'` in a string literal becomes a real line break). An escape
|
|
260
|
+
* that is not valid in a template at all (`\1`, `\x4`) leaves TypeScript no text
|
|
261
|
+
* for that part, so the expression would hold the word `undefined`: `check`
|
|
262
|
+
* refuses it (JS_ESCAPE_INVALID).
|
|
263
|
+
*
|
|
246
264
|
* @param strings - The template's literal parts, supplied by the tag call.
|
|
247
265
|
* @param vals - Interpolated values. An {@link Expr} contributes its reference;
|
|
248
266
|
* anything else is JSON-encoded.
|
|
249
267
|
* @returns An {@link Expr} carrying the composed JavaScript expression.
|
|
250
268
|
* @see tmpl
|
|
269
|
+
* @example
|
|
270
|
+
* ```ts
|
|
271
|
+
* .return({ hasDigit: js`/\\d+/.test(${input('text')})` })
|
|
272
|
+
* ```
|
|
273
|
+
* @enforcedBy JS_ESCAPE_COOKED Write a backslash twice inside `js`: TypeScript
|
|
274
|
+
* reads `\d` as `d` before the SDK sees it.
|
|
251
275
|
*/
|
|
252
276
|
export declare function js(strings: TemplateStringsArray, ...vals: unknown[]): Expr;
|
|
253
277
|
/**
|
|
@@ -261,6 +285,33 @@ export declare function js(strings: TemplateStringsArray, ...vals: unknown[]): E
|
|
|
261
285
|
* @see js
|
|
262
286
|
*/
|
|
263
287
|
export declare function tmpl(strings: TemplateStringsArray, ...vals: unknown[]): Expr;
|
|
288
|
+
/**
|
|
289
|
+
* One backslash escape TypeScript applied inside a `js` template.
|
|
290
|
+
*
|
|
291
|
+
* @internal Recorded by `js`, read by `check`; not an authoring value.
|
|
292
|
+
*/
|
|
293
|
+
export interface CookedEscape {
|
|
294
|
+
/** Offset in `Expr.js` where the cooked text starts. */
|
|
295
|
+
readonly at: number;
|
|
296
|
+
/** The text TypeScript produced, as it sits in `Expr.js` (`d`, U+0008, `undefined`). */
|
|
297
|
+
readonly cooked: string;
|
|
298
|
+
/** What the author typed in its place, read as JavaScript (`\d`). */
|
|
299
|
+
readonly typed: string;
|
|
300
|
+
/** The escape as written, for messages (`\d`). */
|
|
301
|
+
readonly escape: string;
|
|
302
|
+
/**
|
|
303
|
+
* An INVALID template escape (`\1`, `\x4`): TypeScript gives the whole part no
|
|
304
|
+
* cooked value, and `js` writes the word `undefined` in its place.
|
|
305
|
+
*/
|
|
306
|
+
readonly invalid?: true;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* The backslash escapes TypeScript cooked away inside this expression's `js`
|
|
310
|
+
* templates, nested ones included. Empty for anything else.
|
|
311
|
+
*
|
|
312
|
+
* @internal Read by `check`; not an authoring factory.
|
|
313
|
+
*/
|
|
314
|
+
export declare function cookedEscapesOf(val: unknown): readonly CookedEscape[];
|
|
264
315
|
/**
|
|
265
316
|
* Coerce a raw value or Expr into an Expr.
|
|
266
317
|
*
|
package/dist/core/expr.js
CHANGED
|
@@ -260,20 +260,60 @@ export function err(step, field) {
|
|
|
260
260
|
* Interpolated `Expr`s contribute their reference; other values are JSON-encoded.
|
|
261
261
|
* e.g. js`${input('from')} !== ${input('to')}` → `$vars.from !== $vars.to`
|
|
262
262
|
*
|
|
263
|
+
* @remarks
|
|
264
|
+
* **Backslashes are read by TypeScript first.** The expression is the template's
|
|
265
|
+
* COOKED text: TypeScript applies every backslash escape before `js` sees it, the
|
|
266
|
+
* same as in any template literal. So a regular expression typed with one
|
|
267
|
+
* backslash loses it — `` js`/\d+/.test(${x})` `` emits `/d+/.test(…)`, which
|
|
268
|
+
* matches the letter `d`, and `\b` becomes the backspace character, not a word
|
|
269
|
+
* boundary. Write the backslash twice to keep it: `` js`/\\d+/.test(${x})` ``
|
|
270
|
+
* emits `/\d+/.test(…)`. Likewise `` \` `` is a backtick, and `\${` keeps `${` from
|
|
271
|
+
* starting an interpolation.
|
|
272
|
+
* This is the spelling `decompile` writes.
|
|
273
|
+
*
|
|
274
|
+
* `check` reports an escape that changed what the expression means
|
|
275
|
+
* (JS_ESCAPE_COOKED), and an `EXPR_SYNTAX` error names the escape when one broke
|
|
276
|
+
* the parse (`'\n'` in a string literal becomes a real line break). An escape
|
|
277
|
+
* that is not valid in a template at all (`\1`, `\x4`) leaves TypeScript no text
|
|
278
|
+
* for that part, so the expression would hold the word `undefined`: `check`
|
|
279
|
+
* refuses it (JS_ESCAPE_INVALID).
|
|
280
|
+
*
|
|
263
281
|
* @param strings - The template's literal parts, supplied by the tag call.
|
|
264
282
|
* @param vals - Interpolated values. An {@link Expr} contributes its reference;
|
|
265
283
|
* anything else is JSON-encoded.
|
|
266
284
|
* @returns An {@link Expr} carrying the composed JavaScript expression.
|
|
267
285
|
* @see tmpl
|
|
286
|
+
* @example
|
|
287
|
+
* ```ts
|
|
288
|
+
* .return({ hasDigit: js`/\\d+/.test(${input('text')})` })
|
|
289
|
+
* ```
|
|
290
|
+
* @enforcedBy JS_ESCAPE_COOKED Write a backslash twice inside `js`: TypeScript
|
|
291
|
+
* reads `\d` as `d` before the SDK sees it.
|
|
268
292
|
*/
|
|
269
293
|
export function js(strings, ...vals) {
|
|
270
294
|
let out = '';
|
|
295
|
+
const escapes = [];
|
|
271
296
|
strings.forEach((s, i) => {
|
|
297
|
+
// Recorded BEFORE `out` grows, so each offset is where the cooked text lands.
|
|
298
|
+
const raw = strings.raw?.[i];
|
|
299
|
+
if (raw !== undefined)
|
|
300
|
+
for (const e of segmentEscapes(raw, s))
|
|
301
|
+
escapes.push({ ...e, at: out.length + e.at });
|
|
272
302
|
out += s;
|
|
273
|
-
if (i < vals.length)
|
|
303
|
+
if (i < vals.length) {
|
|
304
|
+
const at = out.length;
|
|
274
305
|
out += exprText(vals[i]);
|
|
306
|
+
for (const e of cookedEscapesOf(vals[i]))
|
|
307
|
+
escapes.push({ ...e, at: at + e.at });
|
|
308
|
+
}
|
|
275
309
|
});
|
|
276
|
-
|
|
310
|
+
const lead = out.length - out.trimStart().length;
|
|
311
|
+
const text = out.trim();
|
|
312
|
+
// An escape cooked into whitespace the trim removed is gone from the expression.
|
|
313
|
+
const kept = escapes
|
|
314
|
+
.filter((e) => e.at >= lead && e.at + e.cooked.length <= lead + text.length)
|
|
315
|
+
.map((e) => ({ ...e, at: e.at - lead }));
|
|
316
|
+
return withCookedEscapes(new Expr(text), kept);
|
|
277
317
|
}
|
|
278
318
|
/**
|
|
279
319
|
* Build a JS *string template* (for URLs, messages, etc.).
|
|
@@ -287,16 +327,129 @@ export function js(strings, ...vals) {
|
|
|
287
327
|
*/
|
|
288
328
|
export function tmpl(strings, ...vals) {
|
|
289
329
|
let body = '';
|
|
330
|
+
const escapes = [];
|
|
290
331
|
strings.forEach((s, i) => {
|
|
291
332
|
body += s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
292
|
-
if (i < vals.length)
|
|
333
|
+
if (i < vals.length) {
|
|
334
|
+
// tmpl's OWN escapes are cooked on purpose (`tmpl`a\nb`` is a line break,
|
|
335
|
+
// as in a template literal). Only an interpolated js`…`'s lost escapes
|
|
336
|
+
// travel on, so `check` still sees them once the Expr is nested.
|
|
337
|
+
const at = 1 + body.length + 2; // past the opening backtick and `${`
|
|
293
338
|
body += '${' + exprText(vals[i]) + '}';
|
|
339
|
+
for (const e of cookedEscapesOf(vals[i]))
|
|
340
|
+
escapes.push({ ...e, at: at + e.at });
|
|
341
|
+
}
|
|
294
342
|
});
|
|
295
|
-
return new Expr('`' + body + '`');
|
|
343
|
+
return withCookedEscapes(new Expr('`' + body + '`'), escapes);
|
|
296
344
|
}
|
|
297
345
|
function exprText(val) {
|
|
298
346
|
return val instanceof Expr ? val.js : JSON.stringify(val);
|
|
299
347
|
}
|
|
348
|
+
const COOKED_ESCAPES = Symbol.for('@uipath/maestro-builder-sdk/cookedEscapes');
|
|
349
|
+
/**
|
|
350
|
+
* The backslash escapes TypeScript cooked away inside this expression's `js`
|
|
351
|
+
* templates, nested ones included. Empty for anything else.
|
|
352
|
+
*
|
|
353
|
+
* @internal Read by `check`; not an authoring factory.
|
|
354
|
+
*/
|
|
355
|
+
export function cookedEscapesOf(val) {
|
|
356
|
+
if (val === null || typeof val !== 'object')
|
|
357
|
+
return [];
|
|
358
|
+
return val[COOKED_ESCAPES] ?? [];
|
|
359
|
+
}
|
|
360
|
+
function withCookedEscapes(e, escapes) {
|
|
361
|
+
if (escapes.length)
|
|
362
|
+
Object.defineProperty(e, COOKED_ESCAPES, { value: Object.freeze(escapes), enumerable: false });
|
|
363
|
+
return e;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* The three escapes a `js` template needs to say what it means — and exactly the
|
|
367
|
+
* three `decompile` writes: `\\` (one backslash), `` \` `` (a backtick) and `\${`
|
|
368
|
+
* (so `${` is not an interpolation). A `\$` NOT before `{` is not one of them: a
|
|
369
|
+
* template needs no escape there, and `` js`/\$/` `` cooks a literal-dollar regex
|
|
370
|
+
* into `/$/`, an end anchor. Every other escape is TypeScript's, not the author's
|
|
371
|
+
* JavaScript.
|
|
372
|
+
*/
|
|
373
|
+
function isTemplateEscape(raw, i) {
|
|
374
|
+
const next = raw[i + 1];
|
|
375
|
+
return next === '\\' || next === '`' || (next === '$' && raw[i + 2] === '{');
|
|
376
|
+
}
|
|
377
|
+
const SINGLE_ESCAPES = { b: '\b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v' };
|
|
378
|
+
/**
|
|
379
|
+
* The escapes in one template part, at offsets in its cooked text. `raw` is the
|
|
380
|
+
* part as typed (`strings.raw[i]`), `cooked` what TypeScript made of it — or
|
|
381
|
+
* `undefined` when an escape is not valid in a template (`\1`, `\x4`): the
|
|
382
|
+
* whole part is then lost, and `js` appends the word `undefined` in its place.
|
|
383
|
+
*
|
|
384
|
+
* The cooking rules are ECMAScript's TemplateCharacter grammar. The rebuilt text
|
|
385
|
+
* must equal `cooked`; if it ever does not, nothing is recorded rather than
|
|
386
|
+
* something wrong.
|
|
387
|
+
*/
|
|
388
|
+
function segmentEscapes(raw, cooked) {
|
|
389
|
+
const found = [];
|
|
390
|
+
let built = '';
|
|
391
|
+
let typed = '';
|
|
392
|
+
let invalid;
|
|
393
|
+
for (let i = 0; i < raw.length; i++) {
|
|
394
|
+
const ch = raw[i];
|
|
395
|
+
if (ch !== '\\' || i + 1 >= raw.length) {
|
|
396
|
+
built += ch;
|
|
397
|
+
typed += ch;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const next = raw[i + 1];
|
|
401
|
+
if (isTemplateEscape(raw, i)) {
|
|
402
|
+
built += next;
|
|
403
|
+
typed += next;
|
|
404
|
+
i++;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
let len = 2;
|
|
408
|
+
let value;
|
|
409
|
+
if (next === '\n' || next === '\u2028' || next === '\u2029')
|
|
410
|
+
value = ''; // line continuation
|
|
411
|
+
else if (next in SINGLE_ESCAPES)
|
|
412
|
+
value = SINGLE_ESCAPES[next];
|
|
413
|
+
else if (next === '0' && !/[0-9]/.test(raw[i + 2] ?? ''))
|
|
414
|
+
value = '\0';
|
|
415
|
+
else if (/[0-9]/.test(next))
|
|
416
|
+
value = undefined;
|
|
417
|
+
else if (next === 'x') {
|
|
418
|
+
const hex = /^[0-9a-fA-F]{2}/.exec(raw.slice(i + 2));
|
|
419
|
+
if (hex) {
|
|
420
|
+
value = String.fromCharCode(parseInt(hex[0], 16));
|
|
421
|
+
len = 4;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else if (next === 'u') {
|
|
425
|
+
const m = /^(?:\{([0-9a-fA-F]+)\}|([0-9a-fA-F]{4}))/.exec(raw.slice(i + 2));
|
|
426
|
+
const cp = m ? parseInt(m[1] ?? m[2], 16) : NaN;
|
|
427
|
+
if (m && cp <= 0x10ffff) {
|
|
428
|
+
value = String.fromCodePoint(cp);
|
|
429
|
+
len = 2 + m[0].length;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
// Any other character escapes to itself: `\d` → `d`, `\.` → `.`.
|
|
434
|
+
value = String.fromCodePoint(raw.codePointAt(i + 1));
|
|
435
|
+
len = 1 + value.length;
|
|
436
|
+
}
|
|
437
|
+
const escape = raw.slice(i, i + len);
|
|
438
|
+
if (value === undefined)
|
|
439
|
+
invalid ??= escape;
|
|
440
|
+
else {
|
|
441
|
+
found.push({ at: built.length, cooked: value, typed: escape, escape });
|
|
442
|
+
built += value;
|
|
443
|
+
}
|
|
444
|
+
typed += escape;
|
|
445
|
+
i += len - 1;
|
|
446
|
+
}
|
|
447
|
+
if (cooked === undefined) {
|
|
448
|
+
// `out += undefined` writes the word itself — that is what the expression holds.
|
|
449
|
+
return invalid === undefined ? [] : [{ at: 0, cooked: 'undefined', typed, escape: invalid, invalid: true }];
|
|
450
|
+
}
|
|
451
|
+
return built === cooked ? found : [];
|
|
452
|
+
}
|
|
300
453
|
/**
|
|
301
454
|
* Coerce a raw value or Expr into an Expr.
|
|
302
455
|
*
|
package/dist/core/stable-id.d.ts
CHANGED
|
@@ -24,5 +24,38 @@
|
|
|
24
24
|
* cannot read as a uuid. Coercing to unsigned first is the whole repair; the
|
|
25
25
|
* hash itself is unchanged, so a seed that produced a WELL-FORMED id still
|
|
26
26
|
* produces exactly that id.
|
|
27
|
+
*
|
|
28
|
+
* ## Three seed → uuid algorithms live here, and they are NOT interchangeable
|
|
29
|
+
*
|
|
30
|
+
* `stableId` (connector filter trees, event subscriptions), `fnv1aX4Uuid` (Flow
|
|
31
|
+
* inline-agent resource ids) and `sha256Uuid` (Case entry-point `uniqueId`) each
|
|
32
|
+
* produce ids that are already in shipped artifacts. Replacing one with another
|
|
33
|
+
* renames those resources on the next compile of every existing project. They are
|
|
34
|
+
* owned here so there is one place to look. The golden table in
|
|
35
|
+
* `tests/stable-id.test.ts` pins what each function returns, not which one a call
|
|
36
|
+
* site uses; `tests/cli.test.ts` pins the Case entry point's `uniqueId`. Change one
|
|
37
|
+
* only deliberately, updating that table and any byte-lock fixture it moves.
|
|
27
38
|
*/
|
|
39
|
+
/** A uuid-shaped id derived from `seed`. Every segment is unsigned, including the `>>> 0` fix explained above. */
|
|
28
40
|
export declare function stableId(seed: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* A deterministic v4-shaped uuid from a string.
|
|
43
|
+
*
|
|
44
|
+
* Used for an inline agent's `source` when the author does not pass one. It has to
|
|
45
|
+
* LOOK like a uuid (the platform treats it as an opaque directory name and the
|
|
46
|
+
* designer shows it as a folder) and it has to be STABLE, because a random one would
|
|
47
|
+
* make every recompile a diff of the `.flow` and a rename of a directory. FNV-1a
|
|
48
|
+
* ×4 with the version/variant nibbles pinned — not a cryptographic hash, and it does
|
|
49
|
+
* not need to be: the only requirement is that two different (flow, step) pairs do
|
|
50
|
+
* not collide within one project.
|
|
51
|
+
*
|
|
52
|
+
* Consumer: Flow inline-agent resource ids in `serialize.ts`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function fnv1aX4Uuid(seed: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* A deterministic UUID-shaped id (not a real v4 — the schema only needs a stable string):
|
|
57
|
+
* the first 128 bits of SHA-256(`seed`), no version or variant bits set.
|
|
58
|
+
*
|
|
59
|
+
* Consumer: a Case entry point's `uniqueId` in `entry-points.json` (`case/compile-cli.ts`).
|
|
60
|
+
*/
|
|
61
|
+
export declare function sha256Uuid(seed: string): string;
|
package/dist/core/stable-id.js
CHANGED
|
@@ -24,7 +24,20 @@
|
|
|
24
24
|
* cannot read as a uuid. Coercing to unsigned first is the whole repair; the
|
|
25
25
|
* hash itself is unchanged, so a seed that produced a WELL-FORMED id still
|
|
26
26
|
* produces exactly that id.
|
|
27
|
+
*
|
|
28
|
+
* ## Three seed → uuid algorithms live here, and they are NOT interchangeable
|
|
29
|
+
*
|
|
30
|
+
* `stableId` (connector filter trees, event subscriptions), `fnv1aX4Uuid` (Flow
|
|
31
|
+
* inline-agent resource ids) and `sha256Uuid` (Case entry-point `uniqueId`) each
|
|
32
|
+
* produce ids that are already in shipped artifacts. Replacing one with another
|
|
33
|
+
* renames those resources on the next compile of every existing project. They are
|
|
34
|
+
* owned here so there is one place to look. The golden table in
|
|
35
|
+
* `tests/stable-id.test.ts` pins what each function returns, not which one a call
|
|
36
|
+
* site uses; `tests/cli.test.ts` pins the Case entry point's `uniqueId`. Change one
|
|
37
|
+
* only deliberately, updating that table and any byte-lock fixture it moves.
|
|
27
38
|
*/
|
|
39
|
+
import { createHash } from 'node:crypto';
|
|
40
|
+
/** A uuid-shaped id derived from `seed`. Every segment is unsigned, including the `>>> 0` fix explained above. */
|
|
28
41
|
export function stableId(seed) {
|
|
29
42
|
let h1 = 0x811c9dc5;
|
|
30
43
|
let h2 = 0x01000193;
|
|
@@ -35,3 +48,46 @@ export function stableId(seed) {
|
|
|
35
48
|
const hex = (n, len) => (n >>> 0).toString(16).padStart(8, '0').slice(0, len);
|
|
36
49
|
return `${hex(h1, 8)}-${hex(h2, 4)}-4${hex(h1 >>> 8, 3)}-8${hex(h2 >>> 8, 3)}-${hex(h1 ^ h2, 8)}${hex(h2, 4)}`;
|
|
37
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* A deterministic v4-shaped uuid from a string.
|
|
53
|
+
*
|
|
54
|
+
* Used for an inline agent's `source` when the author does not pass one. It has to
|
|
55
|
+
* LOOK like a uuid (the platform treats it as an opaque directory name and the
|
|
56
|
+
* designer shows it as a folder) and it has to be STABLE, because a random one would
|
|
57
|
+
* make every recompile a diff of the `.flow` and a rename of a directory. FNV-1a
|
|
58
|
+
* ×4 with the version/variant nibbles pinned — not a cryptographic hash, and it does
|
|
59
|
+
* not need to be: the only requirement is that two different (flow, step) pairs do
|
|
60
|
+
* not collide within one project.
|
|
61
|
+
*
|
|
62
|
+
* Consumer: Flow inline-agent resource ids in `serialize.ts`.
|
|
63
|
+
*/
|
|
64
|
+
export function fnv1aX4Uuid(seed) {
|
|
65
|
+
const words = [];
|
|
66
|
+
for (let i = 0; i < 4; i++) {
|
|
67
|
+
let h = 0x811c9dc5 ^ (i * 0x9e3779b9);
|
|
68
|
+
const s = `${seed}#${i}`;
|
|
69
|
+
for (let j = 0; j < s.length; j++) {
|
|
70
|
+
h ^= s.charCodeAt(j);
|
|
71
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
72
|
+
}
|
|
73
|
+
words.push(h >>> 0);
|
|
74
|
+
}
|
|
75
|
+
const hex = words.map((w) => w.toString(16).padStart(8, '0')).join('');
|
|
76
|
+
return [
|
|
77
|
+
hex.slice(0, 8),
|
|
78
|
+
hex.slice(8, 12),
|
|
79
|
+
`4${hex.slice(13, 16)}`,
|
|
80
|
+
`${((parseInt(hex.slice(16, 17), 16) & 0x3) | 0x8).toString(16)}${hex.slice(17, 20)}`,
|
|
81
|
+
hex.slice(20, 32),
|
|
82
|
+
].join('-');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A deterministic UUID-shaped id (not a real v4 — the schema only needs a stable string):
|
|
86
|
+
* the first 128 bits of SHA-256(`seed`), no version or variant bits set.
|
|
87
|
+
*
|
|
88
|
+
* Consumer: a Case entry point's `uniqueId` in `entry-points.json` (`case/compile-cli.ts`).
|
|
89
|
+
*/
|
|
90
|
+
export function sha256Uuid(seed) {
|
|
91
|
+
const h = createHash('sha256').update(seed).digest('hex');
|
|
92
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
93
|
+
}
|
|
@@ -6,6 +6,23 @@ export interface LocatedDiagnostic extends ExprDiagnostic {
|
|
|
6
6
|
}
|
|
7
7
|
/** Every first-level expression problem in a built flow, each stamped with its location. */
|
|
8
8
|
export declare function checkFlowExpressions(built: BuiltFlow): LocatedDiagnostic[];
|
|
9
|
+
/**
|
|
10
|
+
* Every `js` template escape that changed what its expression means
|
|
11
|
+
* (JS_ESCAPE_COOKED), each stamped with where it sits.
|
|
12
|
+
*
|
|
13
|
+
* WHY A GENERIC WALK. The two per-site walks (`walk` above and `check.ts`'s
|
|
14
|
+
* expression collection) each enumerate the positions an expression can sit in,
|
|
15
|
+
* and neither covers all of them — HTTP branch conditions, trigger filters and
|
|
16
|
+
* error-handler bodies each live in only one, or none. This warning is about the
|
|
17
|
+
* `Expr` itself, not about the field it fills, so it visits every `Expr` reachable
|
|
18
|
+
* from the built flow, child flows included: a new position cannot be missed.
|
|
19
|
+
*
|
|
20
|
+
* Only `meaning` effects are reported here. An escape that breaks the parse is
|
|
21
|
+
* named by EXPR_SYNTAX, which already stops `build()`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkCookedEscapes(built: BuiltFlow): (LocatedDiagnostic & {
|
|
24
|
+
step?: string;
|
|
25
|
+
})[];
|
|
9
26
|
/**
|
|
10
27
|
* Thrown by `build()` when the first-level expression check finds errors. Its
|
|
11
28
|
* `message` lists every problem (so a runner that prints `err.message` shows them
|
package/dist/flow-expr-check.js
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
* body is a plain string carrying its own `$vars.*` references; checking those is
|
|
28
28
|
* the same mechanism and a natural next step, deliberately out of this first cut.
|
|
29
29
|
*/
|
|
30
|
-
import { Expr } from './core/expr.js';
|
|
31
|
-
import { checkExpression } from './core/expr-check.js';
|
|
30
|
+
import { Expr, cookedEscapesOf } from './core/expr.js';
|
|
31
|
+
import { checkExpression, cookedEscapeDiagnostics } from './core/expr-check.js';
|
|
32
32
|
const NOUN = 'input, variable, or step';
|
|
33
33
|
/**
|
|
34
34
|
* Every `$vars` root a reference may legitimately resolve to, across the WHOLE
|
|
@@ -108,16 +108,18 @@ function addStepNames(steps, roots) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
function pushExpr(
|
|
112
|
-
if (literal)
|
|
111
|
+
function pushExpr(e, where, roots, out) {
|
|
112
|
+
if (e.literal)
|
|
113
113
|
return; // a literal carries no reference to resolve
|
|
114
|
-
|
|
114
|
+
// The cooked escapes ride along so an EXPR_SYNTAX error can name the one that broke the parse.
|
|
115
|
+
for (const d of checkExpression(e.js, { roots, noun: NOUN, syntax: 'javascript' }, cookedEscapesOf(e))) {
|
|
115
116
|
out.push({ ...d, where });
|
|
117
|
+
}
|
|
116
118
|
}
|
|
117
119
|
/** Reach `Expr`s nested anywhere in an action's inputs (connector/http/… inputs can be structured). */
|
|
118
120
|
function deepExprs(val, where, roots, out) {
|
|
119
121
|
if (val instanceof Expr)
|
|
120
|
-
return pushExpr(val
|
|
122
|
+
return pushExpr(val, where, roots, out);
|
|
121
123
|
if (Array.isArray(val)) {
|
|
122
124
|
for (const v of val)
|
|
123
125
|
deepExprs(v, where, roots, out);
|
|
@@ -140,12 +142,12 @@ function walk(steps, roots, out) {
|
|
|
140
142
|
walk(s.body, roots, out);
|
|
141
143
|
break;
|
|
142
144
|
case 'branch':
|
|
143
|
-
pushExpr(s.cond
|
|
145
|
+
pushExpr(s.cond, s.name, roots, out);
|
|
144
146
|
walk(s.then, roots, out);
|
|
145
147
|
walk(s.otherwise, roots, out);
|
|
146
148
|
break;
|
|
147
149
|
case 'switch': {
|
|
148
|
-
pushExpr(s.on
|
|
150
|
+
pushExpr(s.on, s.name, roots, out);
|
|
149
151
|
// Inside a case (or default) body the discriminant is bound to
|
|
150
152
|
// `$vars.value`, so it is a valid root only there.
|
|
151
153
|
const caseRoots = new Set(roots).add('value');
|
|
@@ -163,14 +165,14 @@ function walk(steps, roots, out) {
|
|
|
163
165
|
walk(c.body, roots, out);
|
|
164
166
|
break;
|
|
165
167
|
case 'loop':
|
|
166
|
-
pushExpr(s.collection
|
|
168
|
+
pushExpr(s.collection, s.name, roots, out);
|
|
167
169
|
if (s.options?.completionCondition) {
|
|
168
|
-
pushExpr(s.options.completionCondition
|
|
170
|
+
pushExpr(s.options.completionCondition, s.name, roots, out);
|
|
169
171
|
}
|
|
170
172
|
walk(s.body, roots, out);
|
|
171
173
|
break;
|
|
172
174
|
case 'doWhile':
|
|
173
|
-
pushExpr(s.condition
|
|
175
|
+
pushExpr(s.condition, s.name, roots, out);
|
|
174
176
|
walk(s.body, roots, out);
|
|
175
177
|
break;
|
|
176
178
|
case 'parallel':
|
|
@@ -179,7 +181,7 @@ function walk(steps, roots, out) {
|
|
|
179
181
|
break;
|
|
180
182
|
case 'return':
|
|
181
183
|
for (const [k, v] of Object.entries(s.values))
|
|
182
|
-
pushExpr(v
|
|
184
|
+
pushExpr(v, `return "${k}"`, roots, out);
|
|
183
185
|
break;
|
|
184
186
|
}
|
|
185
187
|
}
|
|
@@ -193,6 +195,65 @@ export function checkFlowExpressions(built) {
|
|
|
193
195
|
walk(ep.steps, roots, out);
|
|
194
196
|
return out;
|
|
195
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Every `js` template escape that changed what its expression means
|
|
200
|
+
* (JS_ESCAPE_COOKED), each stamped with where it sits.
|
|
201
|
+
*
|
|
202
|
+
* WHY A GENERIC WALK. The two per-site walks (`walk` above and `check.ts`'s
|
|
203
|
+
* expression collection) each enumerate the positions an expression can sit in,
|
|
204
|
+
* and neither covers all of them — HTTP branch conditions, trigger filters and
|
|
205
|
+
* error-handler bodies each live in only one, or none. This warning is about the
|
|
206
|
+
* `Expr` itself, not about the field it fills, so it visits every `Expr` reachable
|
|
207
|
+
* from the built flow, child flows included: a new position cannot be missed.
|
|
208
|
+
*
|
|
209
|
+
* Only `meaning` effects are reported here. An escape that breaks the parse is
|
|
210
|
+
* named by EXPR_SYNTAX, which already stops `build()`.
|
|
211
|
+
*/
|
|
212
|
+
export function checkCookedEscapes(built) {
|
|
213
|
+
const out = [];
|
|
214
|
+
const seen = new WeakSet();
|
|
215
|
+
const root = built;
|
|
216
|
+
const visit = (val, at) => {
|
|
217
|
+
if (val === null || typeof val !== 'object')
|
|
218
|
+
return;
|
|
219
|
+
const escapes = cookedEscapesOf(val);
|
|
220
|
+
if (escapes.length) {
|
|
221
|
+
for (const d of cookedEscapeDiagnostics(val.js, escapes)) {
|
|
222
|
+
out.push({ ...d, where: at.where, ...(at.step !== undefined ? { step: at.step } : {}) });
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (val instanceof Expr || seen.has(val))
|
|
227
|
+
return;
|
|
228
|
+
seen.add(val);
|
|
229
|
+
if (Array.isArray(val)) {
|
|
230
|
+
for (const v of val)
|
|
231
|
+
visit(v, at);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const o = val;
|
|
235
|
+
if (o === root) {
|
|
236
|
+
for (const [k, v] of Object.entries(o))
|
|
237
|
+
visit(v, { where: k, prefix: '' });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// A child flow under a step (a subflow's `spec.child`): its steps are named in its own scope.
|
|
241
|
+
if (at.step !== undefined && Array.isArray(o.steps) && Array.isArray(o.inputs))
|
|
242
|
+
at = { ...at, prefix: `${at.where} › ` };
|
|
243
|
+
if (o.kind === 'return' && o.values && typeof o.values === 'object') {
|
|
244
|
+
for (const [k, v] of Object.entries(o.values))
|
|
245
|
+
visit(v, { ...at, where: `${at.prefix}return "${k}"` });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (typeof o.kind === 'string' && typeof o.name === 'string') {
|
|
249
|
+
at = { ...at, where: `${at.prefix}step "${o.name}"`, step: at.step ?? o.name };
|
|
250
|
+
}
|
|
251
|
+
for (const v of Object.values(o))
|
|
252
|
+
visit(v, at);
|
|
253
|
+
};
|
|
254
|
+
visit(built, { where: 'flow', prefix: '' });
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
196
257
|
/**
|
|
197
258
|
* Thrown by `build()` when the first-level expression check finds errors. Its
|
|
198
259
|
* `message` lists every problem (so a runner that prints `err.message` shows them
|
package/dist/serialize.js
CHANGED
|
@@ -23,7 +23,7 @@ import { declaredExits, hasExits } from './core/step-ports.js';
|
|
|
23
23
|
import { stopsAtEnd } from './flow-step-tree.js';
|
|
24
24
|
import { assertNodeEnvelope } from './core/node-envelope.js';
|
|
25
25
|
import { buildConfiguration } from './config.js';
|
|
26
|
-
import { stableId } from './core/stable-id.js';
|
|
26
|
+
import { fnv1aX4Uuid, stableId } from './core/stable-id.js';
|
|
27
27
|
import { readEventFilter, eventFilterProblem, eventFilterJmes, eventFilterTreeLeaf } from './core/event-filters.js';
|
|
28
28
|
import { FLOW_EXPR_DIALECT } from './core/connector-checks.js';
|
|
29
29
|
import { buildConnectorInputs, connectorInputErrors as coreConnectorInputErrors, multipartBodyFieldName, transportHttpMethod } from './core/connector-inputs.js';
|
|
@@ -3429,37 +3429,6 @@ export function serializeWithSidecars(built, opts = {}) {
|
|
|
3429
3429
|
const flow = serialize(built, { ...opts, sidecars });
|
|
3430
3430
|
return { flow, sidecars };
|
|
3431
3431
|
}
|
|
3432
|
-
/**
|
|
3433
|
-
* A deterministic v4-shaped uuid from a string.
|
|
3434
|
-
*
|
|
3435
|
-
* Used for an inline agent's `source` when the author does not pass one. It has to
|
|
3436
|
-
* LOOK like a uuid (the platform treats it as an opaque directory name and the
|
|
3437
|
-
* designer shows it as a folder) and it has to be STABLE, because a random one would
|
|
3438
|
-
* make every recompile a diff of the `.flow` and a rename of a directory. FNV-1a
|
|
3439
|
-
* ×4 with the version/variant nibbles pinned — not a cryptographic hash, and it does
|
|
3440
|
-
* not need to be: the only requirement is that two different (flow, step) pairs do
|
|
3441
|
-
* not collide within one project.
|
|
3442
|
-
*/
|
|
3443
|
-
function stableUuid(seed) {
|
|
3444
|
-
const words = [];
|
|
3445
|
-
for (let i = 0; i < 4; i++) {
|
|
3446
|
-
let h = 0x811c9dc5 ^ (i * 0x9e3779b9);
|
|
3447
|
-
const s = `${seed}#${i}`;
|
|
3448
|
-
for (let j = 0; j < s.length; j++) {
|
|
3449
|
-
h ^= s.charCodeAt(j);
|
|
3450
|
-
h = Math.imul(h, 0x01000193) >>> 0;
|
|
3451
|
-
}
|
|
3452
|
-
words.push(h >>> 0);
|
|
3453
|
-
}
|
|
3454
|
-
const hex = words.map((w) => w.toString(16).padStart(8, '0')).join('');
|
|
3455
|
-
return [
|
|
3456
|
-
hex.slice(0, 8),
|
|
3457
|
-
hex.slice(8, 12),
|
|
3458
|
-
`4${hex.slice(13, 16)}`,
|
|
3459
|
-
`${((parseInt(hex.slice(16, 17), 16) & 0x3) | 0x8).toString(16)}${hex.slice(17, 20)}`,
|
|
3460
|
-
hex.slice(20, 32),
|
|
3461
|
-
].join('-');
|
|
3462
|
-
}
|
|
3463
3432
|
/**
|
|
3464
3433
|
* `variables.nodes` — one entry per node OUTPUT, derived from the nodes we just
|
|
3465
3434
|
* emitted.
|
|
@@ -5081,7 +5050,7 @@ export function serialize(built, opts = {}) {
|
|
|
5081
5050
|
// name rather than random so a recompile is byte-identical — a v4-shaped uuid
|
|
5082
5051
|
// over a stable hash, because the platform treats it as an opaque id and the
|
|
5083
5052
|
// designer shows it as a folder.
|
|
5084
|
-
const source = s.source ??
|
|
5053
|
+
const source = s.source ?? fnv1aX4Uuid(`${built.id}/${step.name}`);
|
|
5085
5054
|
const node = makeNode(def.nodeType, id, step.name, { parentId, requestedVersion: step.options?.version, def });
|
|
5086
5055
|
// Unlike the published families the inline agent KEEPS no node-level model:
|
|
5087
5056
|
// 29 of 29 deployed instances carry none (the def's `model.source: true` is
|
|
@@ -5193,7 +5162,7 @@ export function serialize(built, opts = {}) {
|
|
|
5193
5162
|
// resource's `id` are the same uuid, because that is what joins them. The
|
|
5194
5163
|
// platform validator says so in its own refusal text — *"the resource id
|
|
5195
5164
|
// from the parent inline agent's `resources[]`"*.
|
|
5196
|
-
const contextResourceId =
|
|
5165
|
+
const contextResourceId = fnv1aX4Uuid(`${built.id}/${step.name}/context/${cx.id}`);
|
|
5197
5166
|
const contextQuery = renderContextQuery(cx.query, scope.rename);
|
|
5198
5167
|
cnode.inputs = {
|
|
5199
5168
|
// REQUIRED, and the platform's validator is what says so — measured
|
|
@@ -5407,7 +5376,7 @@ export function serialize(built, opts = {}) {
|
|
|
5407
5376
|
: tr.kind === 'clientside' ? `clientside.${tr.name}`
|
|
5408
5377
|
: tr.kind === 'httpRequest' ? `httprequest.${tr.name ?? 'default'}`
|
|
5409
5378
|
: tr.key;
|
|
5410
|
-
const toolResourceId =
|
|
5379
|
+
const toolResourceId = fnv1aX4Uuid(`${built.id}/${step.name}/tool/${toolSeed}`);
|
|
5411
5380
|
tnode.inputs = {
|
|
5412
5381
|
source: toolResourceId,
|
|
5413
5382
|
...extraInputs,
|
|
@@ -5450,7 +5419,7 @@ export function serialize(built, opts = {}) {
|
|
|
5450
5419
|
type: f.type,
|
|
5451
5420
|
direction: f.direction,
|
|
5452
5421
|
}));
|
|
5453
|
-
const qsource =
|
|
5422
|
+
const qsource = fnv1aX4Uuid(`${built.id}/${step.name}/escalation/${esc.name}`);
|
|
5454
5423
|
qnode.inputs = {
|
|
5455
5424
|
source: qsource,
|
|
5456
5425
|
name: esc.name,
|
|
@@ -5486,7 +5455,7 @@ export function serialize(built, opts = {}) {
|
|
|
5486
5455
|
path: `${source}/resources/${qsource}/resource.json`,
|
|
5487
5456
|
json: escalationResourceJson(esc, {
|
|
5488
5457
|
id: qsource,
|
|
5489
|
-
channelId:
|
|
5458
|
+
channelId: fnv1aX4Uuid(`${built.id}/${step.name}/escalation/${esc.name}/channel`),
|
|
5490
5459
|
quickFormSchema: qnode.inputs.schema,
|
|
5491
5460
|
}),
|
|
5492
5461
|
});
|
|
@@ -5498,7 +5467,7 @@ export function serialize(built, opts = {}) {
|
|
|
5498
5467
|
const enode = makeNode(edef.nodeType, eid, esc.name, { parentId, def: edef });
|
|
5499
5468
|
enode.display = { ...enode.display, shape: 'circle' };
|
|
5500
5469
|
delete enode.model;
|
|
5501
|
-
const esource =
|
|
5470
|
+
const esource = fnv1aX4Uuid(`${built.id}/${step.name}/escalation/${esc.name}`);
|
|
5502
5471
|
enode.inputs = {
|
|
5503
5472
|
source: esource,
|
|
5504
5473
|
name: esc.name,
|
|
@@ -5535,7 +5504,7 @@ export function serialize(built, opts = {}) {
|
|
|
5535
5504
|
path: `${source}/resources/${esource}/resource.json`,
|
|
5536
5505
|
json: escalationResourceJson(esc, {
|
|
5537
5506
|
id: esource,
|
|
5538
|
-
channelId:
|
|
5507
|
+
channelId: fnv1aX4Uuid(`${built.id}/${step.name}/escalation/${esc.name}/channel`),
|
|
5539
5508
|
}),
|
|
5540
5509
|
});
|
|
5541
5510
|
}
|
|
@@ -5549,7 +5518,7 @@ export function serialize(built, opts = {}) {
|
|
|
5549
5518
|
mnode.display = { ...mnode.display, shape: 'circle' };
|
|
5550
5519
|
delete mnode.model;
|
|
5551
5520
|
mnode.inputs = {
|
|
5552
|
-
source:
|
|
5521
|
+
source: fnv1aX4Uuid(`${built.id}/${step.name}/memory/${m.id}`),
|
|
5553
5522
|
name: m.name,
|
|
5554
5523
|
description: m.description ?? '',
|
|
5555
5524
|
dynamicFewShotLearning: m.dynamicFewShotLearning ?? true,
|
|
@@ -5665,7 +5634,7 @@ export function serialize(built, opts = {}) {
|
|
|
5665
5634
|
// lets the platform do that translation.
|
|
5666
5635
|
const s = spec.inputs;
|
|
5667
5636
|
const id = stepUid(scope, step.name);
|
|
5668
|
-
const source = s.source ??
|
|
5637
|
+
const source = s.source ?? fnv1aX4Uuid(`${built.id}/${step.name}`);
|
|
5669
5638
|
const node = makeNode(NODE_TYPE.conversationalAgent, id, step.name, { parentId, requestedVersion: step.options?.version });
|
|
5670
5639
|
delete node.model;
|
|
5671
5640
|
const st = s.settings ?? {};
|
|
@@ -5758,7 +5727,7 @@ export function serialize(built, opts = {}) {
|
|
|
5758
5727
|
// definition declares what comes back.
|
|
5759
5728
|
const s = spec.inputs;
|
|
5760
5729
|
const id = stepUid(scope, step.name);
|
|
5761
|
-
const source = s.source ??
|
|
5730
|
+
const source = s.source ?? fnv1aX4Uuid(`${built.id}/${step.name}`);
|
|
5762
5731
|
const node = makeNode(NODE_TYPE.voiceAgent, id, step.name, { parentId, requestedVersion: step.options?.version });
|
|
5763
5732
|
// Same rule as the inline agent's: the resource model lives on the
|
|
5764
5733
|
// definition (its `model.source: true` is what makes `source` an input).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/maestro-builder-sdk",
|
|
3
|
-
"version": "6.16.
|
|
3
|
+
"version": "6.16.9",
|
|
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",
|
|
@@ -92,5 +92,5 @@
|
|
|
92
92
|
"@types/node": "^22.7.0",
|
|
93
93
|
"esbuild": "^0.28.1"
|
|
94
94
|
},
|
|
95
|
-
"gitref": "
|
|
95
|
+
"gitref": "dd5b33d6640e176c10be173688ee2767cdd3f251"
|
|
96
96
|
}
|