@uipath/maestro-builder-sdk 6.70.5 → 6.70.7

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.
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Reading another task's output in place: the SDD's `vars.$xref('<stage>',
3
+ * '<task>', '<output>')` marker, and `taskOutput()`, the typed way to write it
4
+ * (builder-sdk#1241).
5
+ *
6
+ * A task's own output is a variable of its own: the Case Designer saves it as
7
+ * `{ name: 'AgentExpenseRequest', id: 'agentExpenseRequest', var:
8
+ * 'agentExpenseRequest' }`, with no root `variables` entry, and a later task
9
+ * reads it as `=vars.agentExpenseRequest`. The id is the plan's, not the
10
+ * author's: it is unique across the plan, so a second task's `Action` output
11
+ * is `action2`. An SDD therefore names an output by where it is produced, and
12
+ * `uip maestro case sdd convert` lowers each marker to `vars.<output id>`
13
+ * (`sdd-convert/xrefs.ts`). Without the lowering here, an author had to copy
14
+ * the value into a case variable the SDD does not declare, and `validate
15
+ * --strict --sdd` refuses each one (`STRICT_SDD_VARIABLE_EXTRA`).
16
+ *
17
+ * The rule lives here so `check` (at `build()`) and `serialize` read a marker
18
+ * the same way. `build()` cannot see the ids, which `serialize` mints, so it
19
+ * resolves the stage and the task, and the output where the task's rows are
20
+ * all authored (a reference-mode task). `serialize` resolves every marker
21
+ * against the rows it wrote and refuses one that names no output.
22
+ */
23
+ /**
24
+ * One argument, read by its own delimiter: `'…'`, `"…"`, `` `…` `` or the curly
25
+ * pair, the four `sdd convert` accepts. A curly pair is two code points, so it
26
+ * gets its own branch instead of a backreference.
27
+ */
28
+ const QUOTED = String.raw `(?:'((?:\\.|[^'\\])*)'|"((?:\\.|[^"\\])*)"|` +
29
+ '`((?:\\\\.|[^`\\\\])*)`' +
30
+ String.raw `|\u201c([^\u201d]*)\u201d)`;
31
+ /** A `vars.$xref(stage, task, output)` marker. Global: use with `replace`/`matchAll` only. */
32
+ const XREF_CALL = new RegExp(String.raw `vars\.\$xref\(\s*${QUOTED}\s*,\s*${QUOTED}\s*,\s*${QUOTED}\s*\)`, 'g');
33
+ /**
34
+ * A property read glued to the marker (`vars.$xref(…).unusualClauses`). The
35
+ * producer may not have run when the reader does, so inside a `=js:`
36
+ * expression the lowering writes the optional chain
37
+ * `vars.<id>?.unusualClauses`, as `sdd convert` does. Only there: `?.` is
38
+ * JavaScript, and nothing shows that the platform's non-js `=vars.<path>`
39
+ * binding reads it, so any other value keeps the plain `.`.
40
+ */
41
+ const DOTTED_READ = /^\.[A-Za-z_$]/;
42
+ const unescape = (value) => value.replace(/\\(.)/g, '$1');
43
+ /** Each marker in `text`, in order, with where it sits. */
44
+ export function taskOutputRefs(text) {
45
+ if (!text.includes('$xref'))
46
+ return [];
47
+ return [...text.matchAll(XREF_CALL)].map((m) => {
48
+ const arg = (from) => unescape(m.slice(from, from + 4).find((v) => v !== undefined) ?? '');
49
+ return { stage: arg(1), task: arg(5), output: arg(9), whole: m[0], index: m.index ?? 0 };
50
+ });
51
+ }
52
+ /**
53
+ * Replace every marker in `text` with what `lower` returns for it. In a `=js:`
54
+ * expression, a marker followed by a property read gets the optional chain
55
+ * (`vars.<id>?.field`); any other value keeps the plain read (`vars.<id>.field`).
56
+ *
57
+ * @param text - The expression.
58
+ * @param lower - The variable a marker reads (`agentExpenseRequest`), or `undefined` to keep the marker.
59
+ */
60
+ export function lowerTaskOutputRefs(text, lower) {
61
+ if (!text.includes('$xref'))
62
+ return text;
63
+ const js = /^\s*=js:/.test(text);
64
+ let out = '';
65
+ let at = 0;
66
+ for (const ref of taskOutputRefs(text)) {
67
+ const after = text.slice(ref.index + ref.whole.length);
68
+ const variable = lower(ref);
69
+ // `undefined` keeps the marker as written (a reference to a stand-in; see `resolveInPlan`).
70
+ out += variable === undefined ? text.slice(at, ref.index) + ref.whole : `${text.slice(at, ref.index)}vars.${variable}${js && DOTTED_READ.test(after) ? '?' : ''}`;
71
+ at = ref.index + ref.whole.length;
72
+ }
73
+ return out + text.slice(at);
74
+ }
75
+ /**
76
+ * The one candidate `name` picks out: an exact match first, then a
77
+ * case-insensitive one (an SDD addresses a stage or an output in any case).
78
+ * `undefined` when none does; `'ambiguous'` when more than one does.
79
+ */
80
+ export function pickByName(items, name, names) {
81
+ for (const same of [(a) => a === name, (a) => a.toLowerCase() === name.toLowerCase()]) {
82
+ const hits = [...new Set(items.filter((item) => names(item).some((n) => n !== undefined && same(n))))];
83
+ if (hits.length === 1)
84
+ return hits[0];
85
+ if (hits.length > 1)
86
+ return 'ambiguous';
87
+ }
88
+ return undefined;
89
+ }
90
+ /**
91
+ * The row an output reference names among one task's rows: by the field it
92
+ * reads (`name`) or by the variable it writes (`var`).
93
+ */
94
+ export function pickOutputRow(rows, output) {
95
+ return pickByName(rows, output, (row) => [row.name, row.var]);
96
+ }
97
+ /** The refusal message for a marker that resolves to nothing. */
98
+ export function unresolvedTaskOutputRef(ref, why) {
99
+ return `taskOutput(${JSON.stringify(ref.stage)}, ${JSON.stringify(ref.task)}, ${JSON.stringify(ref.output)}) ${why}. ` +
100
+ 'A task-output reference names the stage label, the task name and the output (the result field it reads, or the key it has in `.outputs()`).';
101
+ }
102
+ /**
103
+ * The marker for a reference: `vars.$xref('<stage>','<task>','<output>')`,
104
+ * each argument single-quoted with its quotes and backslashes escaped. What
105
+ * `taskOutput()` returns, and what decompile writes back.
106
+ */
107
+ export function taskOutputMarker(stage, task, output) {
108
+ const quote = (value) => `'${value.replace(/[\\']/g, '\\$&')}'`;
109
+ return `vars.$xref(${quote(stage)},${quote(task)},${quote(output)})`;
110
+ }
111
+ /** Every stage task's output rows in a written caseplan, in document order. */
112
+ export function planTaskOutputs(plan) {
113
+ const found = [];
114
+ const nodes = plan?.nodes;
115
+ for (const node of Array.isArray(nodes) ? nodes : []) {
116
+ const data = node?.data;
117
+ if (typeof data?.label !== 'string' || !Array.isArray(data.tasks))
118
+ continue;
119
+ for (const lane of data.tasks)
120
+ for (const task of Array.isArray(lane) ? lane : []) {
121
+ if (typeof task?.displayName !== 'string')
122
+ continue;
123
+ const rows = Array.isArray(task.data?.outputs) ? task.data.outputs : [];
124
+ found.push({
125
+ stage: data.label,
126
+ task: task.displayName,
127
+ rows: rows.filter((row) => row && typeof row === 'object'),
128
+ ...(task.data?.standIn === true ? { standIn: true } : {}),
129
+ });
130
+ }
131
+ }
132
+ return found;
133
+ }
134
+ /**
135
+ * What a reference reads in a written plan: the variable of the row it names
136
+ * (`vars.<id>`, or a custom row's `var`, which has no id), or why it names none.
137
+ */
138
+ export function resolveInPlan(tasks, ref) {
139
+ const stage = pickByName([...new Set(tasks.map((t) => t.stage))], ref.stage, (label) => [label]);
140
+ if (stage === undefined)
141
+ return { why: `names no stage labelled ${JSON.stringify(ref.stage)}` };
142
+ if (stage === 'ambiguous')
143
+ return { why: `names a stage label that more than one stage answers to` };
144
+ const task = pickByName(tasks.filter((t) => t.stage === stage), ref.task, (t) => [t.task]);
145
+ if (task === undefined)
146
+ return { why: `names no task ${JSON.stringify(ref.task)} in stage ${JSON.stringify(stage)}` };
147
+ if (task === 'ambiguous')
148
+ return { why: `names a task that more than one task in stage ${JSON.stringify(stage)} answers to` };
149
+ // The named task was refused and stood in (collect mode). Its refusal is the
150
+ // finding; its outputs were never written, so there is nothing to judge the
151
+ // reference against here (`build()` already judged it against the authored ones).
152
+ if (task.standIn)
153
+ return { standIn: true };
154
+ const row = pickOutputRow(task.rows, ref.output);
155
+ const offered = [...new Set(task.rows.map((r) => r.name).filter((n) => typeof n === 'string'))];
156
+ if (row === undefined)
157
+ return { why: `names no output of task ${JSON.stringify(task.task)}, whose outputs are ${offered.length ? offered.map((n) => JSON.stringify(n)).join(', ') : 'none'}` };
158
+ if (row === 'ambiguous')
159
+ return { why: `names more than one output of task ${JSON.stringify(task.task)}; name it by the variable it writes instead` };
160
+ const variable = typeof row.id === 'string' && row.id ? row.id : row.var;
161
+ if (typeof variable !== 'string' || !variable)
162
+ return { why: `names an output of task ${JSON.stringify(task.task)} that writes no variable` };
163
+ return { variable };
164
+ }
165
+ /**
166
+ * Turn each `vars.<output id>` read in `text` that only a task-output reference
167
+ * can say back into `vars.$xref(…)`, for decompile. `byId` maps such an id to
168
+ * the reference that names it.
169
+ */
170
+ export function raiseTaskOutputReads(text, byId) {
171
+ if (!byId.size || !text.includes('vars.'))
172
+ return text;
173
+ return text.replace(/(?<![\w$.])vars\.([A-Za-z_$][\w$]*)(?![\w$])/g, (whole, id) => {
174
+ const ref = byId.get(id);
175
+ return ref ? taskOutputMarker(ref.stage, ref.task, ref.output) : whole;
176
+ });
177
+ }
178
+ /**
179
+ * The output ids a decompiled source can only read back by reference: a row
180
+ * whose id is not the variable it writes (a reassigned extract, a Designer row
181
+ * such as `{ id: 'oA1b2C3d4', var: 'AgentExpenseRequest' }`). Recompiling mints
182
+ * that id afresh, and no declaration makes `vars.<id>` readable, so a
183
+ * `vars.<id>` read in the plan goes back to the source as the reference that
184
+ * resolves to the same row. A read of an id that is also a variable (an
185
+ * own-name row, a root variable) is left as written. A row the reference
186
+ * could not name uniquely is left as written too.
187
+ */
188
+ export function referableOutputIds(plan, rootNames) {
189
+ const tasks = planTaskOutputs(plan);
190
+ const vars = new Set(rootNames);
191
+ for (const task of tasks)
192
+ for (const row of task.rows)
193
+ if (typeof row.var === 'string')
194
+ vars.add(row.var);
195
+ const byId = new Map();
196
+ for (const task of tasks)
197
+ for (const row of task.rows) {
198
+ if (typeof row.id !== 'string' || !row.id || row.id === row.var || vars.has(row.id))
199
+ continue;
200
+ for (const output of [row.name, row.var]) {
201
+ if (typeof output !== 'string' || !output)
202
+ continue;
203
+ const ref = { stage: task.stage, task: task.task, output };
204
+ const resolved = resolveInPlan(tasks, ref);
205
+ if ('variable' in resolved && resolved.variable === row.id) {
206
+ byId.set(row.id, ref);
207
+ break;
208
+ }
209
+ }
210
+ }
211
+ return byId;
212
+ }
@@ -24,7 +24,7 @@
24
24
  */
25
25
  import { diagnosticLine, reportDiagnostics, reportResult, thrownDiagnostic } from './cli-result.js';
26
26
  import { writeJsonFile } from './core/json-file.js';
27
- import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
27
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
28
28
  import { basename, dirname, join, relative, resolve } from 'node:path';
29
29
  import { resolveFlowFile, loadBuiltFlow } from './load.js';
30
30
  import { serializeWithSidecars } from './serialize.js';
@@ -36,7 +36,7 @@ import { connectionResources, inlineAgentResources, mergeBindingsV2, refreshInli
36
36
  import { readBindingsV2, readInlineAgentFiles } from './bindings-v2-files.js';
37
37
  import { ensureTypeScriptRuntime } from './node-runtime.js';
38
38
  import { defaultBindingsFile } from './workdir.js';
39
- import { isWithin } from './path-within.js';
39
+ import { isWithin, physicalPath } from './path-within.js';
40
40
  import { flowSources, resourceSourcesOfOtherFlows } from './project-resource-users.js';
41
41
  import { fnv1aX4Uuid } from './core/stable-id.js';
42
42
  import { CompileRefusal } from './core/compile-refusal.js';
@@ -86,7 +86,20 @@ export async function run(argv) {
86
86
  // BYOA-only event on a connection `prepare` never checked
87
87
  // (EVENT_BYOA_UNVERIFIED): printed in the same `✗ [CODE]` shape `check` uses
88
88
  // for it. Nothing is written.
89
- const out = flagValue(argv, '-o') ?? `${built.name || basename(file).replace(/\.flow\.ts$|\.ts$/, '')}.flow`;
89
+ const explicitOut = flagValue(argv, '-o');
90
+ const out = explicitOut ?? `${built.name || basename(file).replace(/\.flow\.ts$|\.ts$/, '')}.flow`;
91
+ // The default is named after the flow, which decompile copies from the input: keep it inside cwd.
92
+ if (explicitOut === undefined) {
93
+ const target = physicalPath(resolve(out));
94
+ if (target === undefined || !isWithin(realpathSync.native(process.cwd()), target)) {
95
+ const message = `refusing to write ${out} outside the working directory: without -o the output is named after`
96
+ + ` the flow (${JSON.stringify(built.name)}). Pass -o <file>.flow to choose where it is written`;
97
+ console.error(`compile: ${message}; not emitting.`);
98
+ diagnostics.push({ level: 'error', code: 'FLOW_NAME_OUTSIDE_WORKING_DIR', message });
99
+ reportDiagnostics(diagnostics);
100
+ return 1;
101
+ }
102
+ }
90
103
  const outDir = dirname(resolve(out));
91
104
  // ONE FOLDER, ONE FLOW (#1184). An agent's `source` is the name of the folder
92
105
  // beside the `.flow` that holds its `agent.json` and `resources/`. Decompile
@@ -640,6 +640,7 @@ export declare const REFUSALS: {
640
640
  readonly TASK_OUTPUTS_NO_RESPONSE: "lowering";
641
641
  readonly TASK_OUTPUTS_PLACEHOLDER_EVENT: "lowering";
642
642
  readonly TASK_OUTPUTS_UNSUPPORTED: "lowering";
643
+ readonly TASK_OUTPUT_REF_UNRESOLVED: "lowering";
643
644
  readonly TASK_OUTPUT_SOURCE_AND_ASSIGN: "lowering";
644
645
  readonly TASK_REENTRY_IGNORED: "lint";
645
646
  readonly TASK_TIMER_OUT_OF_RANGE: "lint";
@@ -692,6 +692,7 @@ export const REFUSALS = {
692
692
  TASK_OUTPUTS_NO_RESPONSE: 'lowering',
693
693
  TASK_OUTPUTS_PLACEHOLDER_EVENT: 'lowering',
694
694
  TASK_OUTPUTS_UNSUPPORTED: 'lowering',
695
+ TASK_OUTPUT_REF_UNRESOLVED: 'lowering',
695
696
  TASK_OUTPUT_SOURCE_AND_ASSIGN: 'lowering',
696
697
  TASK_REENTRY_IGNORED: 'lint',
697
698
  TASK_TIMER_OUT_OF_RANGE: 'lint',
@@ -1,20 +1,3 @@
1
- /**
2
- * path-within — the one path-containment test for anything that writes files.
3
- *
4
- * "Is `child` inside `parent`?" looks like a string-prefix question and is not
5
- * one. `child.startsWith(parent + '/')` is correct only where the separator is
6
- * `/`: on Windows `path.resolve` returns `C:\proj\P\x`, the check refuses every
7
- * path, and compile refused every inline agent's `<source>/agent.json`
8
- * (flow-builder-sdk#946). A prefix test without a trailing separator has the
9
- * opposite bug: it accepts the sibling `C:\proj\P2\x` as inside `C:\proj\P`.
10
- *
11
- * So the test is `relative(parent, child)` instead, which gets both right on
12
- * every platform, and Windows' case-insensitive drive letters and
13
- * cross-drive paths (`relative` returns the absolute `D:\x` there) with them.
14
- *
15
- * The path implementation is a parameter so the Windows rules can be tested on
16
- * a Linux runner (`isWithin(a, b, path.win32)`). Callers leave it out.
17
- */
18
1
  import * as nodePath from 'node:path';
19
2
  /** The slice of `node:path` the test needs; `path.win32` and `path.posix` both fit. */
20
3
  export type PathApi = Pick<typeof nodePath, 'relative' | 'isAbsolute' | 'sep'>;
@@ -31,3 +14,18 @@ export type PathApi = Pick<typeof nodePath, 'relative' | 'isAbsolute' | 'sep'>;
31
14
  * isWithin('C:\\proj\\P', 'D:\\agent.json', path.win32) // false — other drive
32
15
  */
33
16
  export declare function isWithin(parent: string, child: string, path?: PathApi): boolean;
17
+ /**
18
+ * Where `dir` lands on disk: the real path of its deepest ancestor that
19
+ * exists, every link on the way followed, then the rest of `dir`, which does
20
+ * not exist yet and so holds no link. `undefined` when that ancestor's real
21
+ * path cannot be read: a link to nothing, or a loop of links. A caller treats
22
+ * `undefined` as outside.
23
+ *
24
+ * The agent SDK's CLI keeps a copy of this function for its default output
25
+ * folder, so both CLIs follow links the same way.
26
+ *
27
+ * @example
28
+ * // ./work/sw-basic/l is a link to /elsewhere, and X does not exist:
29
+ * physicalPath('/cwd/work/sw-basic/l/X') // '/elsewhere/X'
30
+ */
31
+ export declare function physicalPath(dir: string): string | undefined;
@@ -12,10 +12,16 @@
12
12
  * every platform, and Windows' case-insensitive drive letters and
13
13
  * cross-drive paths (`relative` returns the absolute `D:\x` there) with them.
14
14
  *
15
+ * `isWithin` reads only the spelling of its paths, so a link inside `parent`
16
+ * that points out of it passes. A check that guards a write compares
17
+ * `physicalPath`s instead.
18
+ *
15
19
  * The path implementation is a parameter so the Windows rules can be tested on
16
20
  * a Linux runner (`isWithin(a, b, path.win32)`). Callers leave it out.
17
21
  */
22
+ import { lstatSync, realpathSync } from 'node:fs';
18
23
  import * as nodePath from 'node:path';
24
+ import { basename, dirname, join } from 'node:path';
19
25
  /**
20
26
  * `child` is `parent` itself or somewhere beneath it.
21
27
  *
@@ -39,3 +45,41 @@ export function isWithin(parent, child, path = nodePath) {
39
45
  // Windows: a child on another drive has no relative spelling.
40
46
  return !path.isAbsolute(rel);
41
47
  }
48
+ /**
49
+ * Where `dir` lands on disk: the real path of its deepest ancestor that
50
+ * exists, every link on the way followed, then the rest of `dir`, which does
51
+ * not exist yet and so holds no link. `undefined` when that ancestor's real
52
+ * path cannot be read: a link to nothing, or a loop of links. A caller treats
53
+ * `undefined` as outside.
54
+ *
55
+ * The agent SDK's CLI keeps a copy of this function for its default output
56
+ * folder, so both CLIs follow links the same way.
57
+ *
58
+ * @example
59
+ * // ./work/sw-basic/l is a link to /elsewhere, and X does not exist:
60
+ * physicalPath('/cwd/work/sw-basic/l/X') // '/elsewhere/X'
61
+ */
62
+ export function physicalPath(dir) {
63
+ const rest = [];
64
+ let at = dir;
65
+ for (;;) {
66
+ try {
67
+ lstatSync(at);
68
+ break;
69
+ }
70
+ catch {
71
+ // Missing: its parent, then.
72
+ }
73
+ const parent = dirname(at);
74
+ if (parent === at)
75
+ break;
76
+ rest.unshift(basename(at));
77
+ at = parent;
78
+ }
79
+ try {
80
+ return join(realpathSync.native(at), ...rest);
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.70.5",
3
+ "version": "6.70.7",
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",
@@ -100,5 +100,5 @@
100
100
  "@types/node": "^22.7.0",
101
101
  "esbuild": "^0.28.1"
102
102
  },
103
- "gitref": "fa9dc7bf7b81b8dfd7fe6090beb21100879d79f6"
103
+ "gitref": "31471f814e8ca706a02e96e3dbceb39641584de6"
104
104
  }