@uipath/maestro-builder-sdk 6.31.4 → 6.31.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compile-cli.js +131 -8
- package/dist/merge.d.ts +3 -1
- package/dist/merge.js +69 -29
- package/dist/project-resource-users.d.ts +8 -0
- package/dist/project-resource-users.js +25 -11
- package/dist/serialize.d.ts +12 -0
- package/dist/serialize.js +15 -7
- package/package.json +2 -2
package/dist/compile-cli.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { reportDiagnostics, reportResult, thrownDiagnostic } from './cli-result.js';
|
|
21
21
|
import { writeJsonFile } from './core/json-file.js';
|
|
22
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
22
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
23
23
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
24
24
|
import { resolveFlowFile, loadBuiltFlow } from './load.js';
|
|
25
25
|
import { serializeWithSidecars, compileOwnsResource } from './serialize.js';
|
|
@@ -32,9 +32,12 @@ import { readBindingsV2, readInlineAgentFiles } from './bindings-v2-files.js';
|
|
|
32
32
|
import { ensureTypeScriptRuntime } from './node-runtime.js';
|
|
33
33
|
import { defaultBindingsFile } from './workdir.js';
|
|
34
34
|
import { isWithin } from './path-within.js';
|
|
35
|
-
import { resourceSourcesOfOtherFlows } from './project-resource-users.js';
|
|
35
|
+
import { flowSources, resourceSourcesOfOtherFlows } from './project-resource-users.js';
|
|
36
|
+
import { fnv1aX4Uuid } from './core/stable-id.js';
|
|
36
37
|
import { BindingRefusal } from './core/binding-messages.js';
|
|
37
38
|
import { ByoaRefusal } from './core/byoa.js';
|
|
39
|
+
/** An agent folder name: a uuid, any case. */
|
|
40
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
38
41
|
export async function run(argv) {
|
|
39
42
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
40
43
|
console.error('usage: compile <flow.ts | BaseName> [-o out.flow] [--library <dir>] [--bindings <file>]');
|
|
@@ -93,10 +96,57 @@ export async function run(argv) {
|
|
|
93
96
|
// (EVENT_BYOA_UNVERIFIED): the check pass above reports each first, and this is
|
|
94
97
|
// the same refusal in the same `✗ [CODE]` shape when that pass was skipped
|
|
95
98
|
// (`--no-check`, or an emit-only project). Nothing is written.
|
|
99
|
+
const out = flagValue(argv, '-o') ?? `${built.name || basename(file).replace(/\.flow\.ts$|\.ts$/, '')}.flow`;
|
|
100
|
+
const outDir = dirname(resolve(out));
|
|
101
|
+
// ONE FOLDER, ONE FLOW (#1184). An agent's `source` is the name of the folder
|
|
102
|
+
// beside the `.flow` that holds its `agent.json` and `resources/`. Decompile
|
|
103
|
+
// keeps a canvas agent's `source` (the sidecar must still resolve), so
|
|
104
|
+
// compiling that source to a SECOND `.flow` in the same project would write
|
|
105
|
+
// into the first flow's folder: its `agent.json` is overwritten with the new
|
|
106
|
+
// flow's agent, and `uip agent refresh` for the new flow declares every
|
|
107
|
+
// resource the first flow keeps there. The product never shares a folder
|
|
108
|
+
// either: Studio Web mints a fresh uuid for an imported flow whose agent the
|
|
109
|
+
// project already owns, copies the folder and restamps `projectId`
|
|
110
|
+
// (flow-workbench MST-14919). So does compile: an agent whose `source`
|
|
111
|
+
// another `.flow` in this Flow project claims is written to its own folder,
|
|
112
|
+
// derived from that `source` and this file's name, so a recompile lands in
|
|
113
|
+
// the same one. Outside a Flow project (`project.uiproj`) nothing is a
|
|
114
|
+
// deliverable: the brownfield loop writes its baseline/edited candidates
|
|
115
|
+
// beside the original on purpose, and they are left as they were.
|
|
116
|
+
//
|
|
117
|
+
// Two rules keep the choice stable across later edits to the project:
|
|
118
|
+
// - The OWNER keeps its folder. A source the existing `out` file already
|
|
119
|
+
// names stays put, even if another `.flow` names it too (one compiled
|
|
120
|
+
// into it before this rule existed): the flow being recompiled is the one
|
|
121
|
+
// that already lives there, so it is never the one moved.
|
|
122
|
+
// - A re-home is STICKY. Once the re-homed folder exists, the step keeps
|
|
123
|
+
// writing there even after the flow that claimed the old folder is
|
|
124
|
+
// deleted or renamed; otherwise the next compile would move back into the
|
|
125
|
+
// old folder (and its leftover resources) and orphan the new one.
|
|
126
|
+
const inProject = existsSync(join(outDir, 'project.uiproj'));
|
|
127
|
+
const claimed = inProject ? sourcesOfOtherFlows(outDir, resolve(out)) : new Map();
|
|
128
|
+
const ownSources = inProject ? new Set(flowSources(readJsonOrUndefined(resolve(out))).map((s) => s.toLowerCase())) : new Set();
|
|
129
|
+
const rehomed = new Map();
|
|
130
|
+
const agentSource = (source, step) => {
|
|
131
|
+
// Only a uuid names a folder (as in Studio Web); anything else is left to
|
|
132
|
+
// the sidecar-path check below rather than read from here.
|
|
133
|
+
if (!inProject || !UUID.test(source))
|
|
134
|
+
return source;
|
|
135
|
+
const key = source.toLowerCase();
|
|
136
|
+
if (ownSources.has(key))
|
|
137
|
+
return source;
|
|
138
|
+
const to = fnv1aX4Uuid(`${key}/${basename(out)}`);
|
|
139
|
+
const owner = claimed.get(key);
|
|
140
|
+
if (owner === undefined)
|
|
141
|
+
return existsSync(join(outDir, to, 'agent.json')) ? to : source;
|
|
142
|
+
if (!rehomed.has(source))
|
|
143
|
+
rehomed.set(source, { from: source, to, step, owner });
|
|
144
|
+
return to;
|
|
145
|
+
};
|
|
96
146
|
const warnings = [];
|
|
97
147
|
let emitted;
|
|
98
148
|
try {
|
|
99
|
-
emitted = serializeWithSidecars(built, { library, bindings, warn: (w) => warnings.push(w) });
|
|
149
|
+
emitted = serializeWithSidecars(built, { library, bindings, warn: (w) => warnings.push(w), agentSource });
|
|
100
150
|
}
|
|
101
151
|
catch (error) {
|
|
102
152
|
for (const w of warnings)
|
|
@@ -119,7 +169,6 @@ export async function run(argv) {
|
|
|
119
169
|
for (const w of warnings)
|
|
120
170
|
console.error(` ⚠ [${w.code}] ${w.message}`);
|
|
121
171
|
diagnostics.push(...warnings.map(serializeWarningDiagnostic));
|
|
122
|
-
const out = flagValue(argv, '-o') ?? `${built.name || basename(file).replace(/\.flow\.ts$|\.ts$/, '')}.flow`;
|
|
123
172
|
// An inline agent is TWO artifacts: the node, and the agent's own configuration
|
|
124
173
|
// at `<source>/agent.json` — which is where flow-v1's serializer points the
|
|
125
174
|
// runtime (`entryPoint: "<source>/agent.json"`, resolved relative to the
|
|
@@ -131,7 +180,6 @@ export async function run(argv) {
|
|
|
131
180
|
//
|
|
132
181
|
// Every sidecar is checked before anything is written; success is reported
|
|
133
182
|
// only once every file is on disk.
|
|
134
|
-
const outDir = dirname(resolve(out));
|
|
135
183
|
const targets = sidecars.map((s) => ({ sidecar: s, target: resolve(outDir, s.path) }));
|
|
136
184
|
const escaping = targets.find(({ target }) => target === outDir || !isWithin(outDir, target));
|
|
137
185
|
if (escaping) {
|
|
@@ -152,6 +200,20 @@ export async function run(argv) {
|
|
|
152
200
|
console.log(`compile: wrote ${sidecar.path}`);
|
|
153
201
|
written.push(target);
|
|
154
202
|
}
|
|
203
|
+
// A re-homed agent's folder starts as a copy of the folder it came from, minus
|
|
204
|
+
// what this compile just wrote (its `agent.json`, the resources it derives)
|
|
205
|
+
// and minus every `resources/<id>/` this flow's nodes do not name — copying
|
|
206
|
+
// those would hand the new flow's agent the old flow's index again, which is
|
|
207
|
+
// the bug. A file already in the new folder is the new flow's own and is kept.
|
|
208
|
+
if (rehomed.size > 0) {
|
|
209
|
+
const referenced = new Set(flowSources(flowJson).map((s) => s.toLowerCase()));
|
|
210
|
+
for (const { from, to, step, owner } of rehomed.values()) {
|
|
211
|
+
const copied = seedFolder(join(outDir, from), join(outDir, to), referenced);
|
|
212
|
+
console.log(`compile: agent "${step}" uses its own folder ${to}: ${from} belongs to ${owner}`
|
|
213
|
+
+ (copied > 0 ? ` (copied ${copied} file(s) from it)` : '')
|
|
214
|
+
+ `. To make this explicit, set source: "${to}" on "${step}" in the .flow.ts.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
155
217
|
reportResult({ path: resolve(out), nodes: flowJson.nodes.length, edges: flowJson.edges.length, sidecars: written });
|
|
156
218
|
// PRUNE the agent resources this compile owns but did not write this time.
|
|
157
219
|
// Compile otherwise only ever ADDS files, and an agent resource is keyed by a
|
|
@@ -176,7 +238,11 @@ export async function run(argv) {
|
|
|
176
238
|
// canvas resources there are still that flow's. A resource another
|
|
177
239
|
// `.flow` beside this one references by `inputs.source` is never pruned.
|
|
178
240
|
const removedResources = [];
|
|
179
|
-
const referencedElsewhere =
|
|
241
|
+
const referencedElsewhere = sourcesOfOtherFlows(outDir, resolve(out));
|
|
242
|
+
// ... and neither is one THIS flow still names: a re-homed agent's folder is
|
|
243
|
+
// seeded with the resources its nodes reference (see `seedFolder`), and a
|
|
244
|
+
// designer resource a node here points at is in use, whatever its type.
|
|
245
|
+
const referencedHere = new Set(flowSources(flowJson).map((s) => s.toLowerCase()));
|
|
180
246
|
const owned = new Set(sidecars
|
|
181
247
|
.filter((s) => /\/resources\/[^/]+\/resource\.json$/.test(s.path))
|
|
182
248
|
.map((s) => resolve(outDir, s.path)));
|
|
@@ -189,9 +255,9 @@ export async function run(argv) {
|
|
|
189
255
|
continue;
|
|
190
256
|
for (const entry of readdirSync(resourcesDir)) {
|
|
191
257
|
const file = join(resourcesDir, entry, 'resource.json');
|
|
192
|
-
if (!existsSync(file) || owned.has(file))
|
|
258
|
+
if (!existsSync(file) || owned.has(file) || referencedHere.has(entry.toLowerCase()))
|
|
193
259
|
continue;
|
|
194
|
-
const user = referencedElsewhere.get(entry);
|
|
260
|
+
const user = referencedElsewhere.get(entry.toLowerCase());
|
|
195
261
|
if (user !== undefined) {
|
|
196
262
|
console.log(`compile: kept ${join('resources', entry, 'resource.json')}: ${user} still references it`);
|
|
197
263
|
continue;
|
|
@@ -251,3 +317,60 @@ function serializeWarningDiagnostic(w) {
|
|
|
251
317
|
return { level: 'warning', code: w.code, message: w.message };
|
|
252
318
|
}
|
|
253
319
|
runWhenInvokedDirectly(import.meta.url, 'compile', run);
|
|
320
|
+
/** A JSON file's contents, or `undefined` when it is missing or unreadable. */
|
|
321
|
+
function readJsonOrUndefined(path) {
|
|
322
|
+
try {
|
|
323
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* `resourceSourcesOfOtherFlows`, keyed lower-case: folder names keep their
|
|
331
|
+
* case, claims must not. The first file to claim a source wins, as there.
|
|
332
|
+
*/
|
|
333
|
+
function sourcesOfOtherFlows(dir, self) {
|
|
334
|
+
const users = new Map();
|
|
335
|
+
for (const [src, f] of resourceSourcesOfOtherFlows(dir, self)) {
|
|
336
|
+
if (!users.has(src.toLowerCase()))
|
|
337
|
+
users.set(src.toLowerCase(), f);
|
|
338
|
+
}
|
|
339
|
+
return users;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Copy agent folder `from` into `to`, for an agent re-homed off a folder another
|
|
343
|
+
* flow owns. Skipped: the top-level `agent.json` (compile writes its own, with
|
|
344
|
+
* the new identity), any `resources/<id>/` whose id is not in `referenced`, any
|
|
345
|
+
* file already present in `to`, and anything that is not a plain file or
|
|
346
|
+
* folder. Returns how many files were copied; a missing `from` copies nothing.
|
|
347
|
+
*/
|
|
348
|
+
function seedFolder(from, to, referenced) {
|
|
349
|
+
let copied = 0;
|
|
350
|
+
const walk = (rel) => {
|
|
351
|
+
let entries;
|
|
352
|
+
try {
|
|
353
|
+
entries = readdirSync(join(from, rel), { withFileTypes: true });
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
for (const e of entries) {
|
|
359
|
+
const r = rel === '' ? e.name : join(rel, e.name);
|
|
360
|
+
if (rel === '' && e.name === 'agent.json')
|
|
361
|
+
continue;
|
|
362
|
+
if (rel === 'resources' && !referenced.has(e.name.toLowerCase()))
|
|
363
|
+
continue;
|
|
364
|
+
if (e.isDirectory()) {
|
|
365
|
+
walk(r);
|
|
366
|
+
}
|
|
367
|
+
else if (e.isFile() && !existsSync(join(to, r))) {
|
|
368
|
+
mkdirSync(dirname(join(to, r)), { recursive: true });
|
|
369
|
+
copyFileSync(join(from, r), join(to, r));
|
|
370
|
+
copied++;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
walk('');
|
|
375
|
+
return copied;
|
|
376
|
+
}
|
package/dist/merge.d.ts
CHANGED
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
* preserved from the original; `subflows` and edges (with their ids) come from
|
|
29
29
|
* `edited`; `definitions` and `bindings` are unioned
|
|
30
30
|
* (original wins on conflict, since the original's are the tenant-resolved
|
|
31
|
-
* authority). A binding row's identity is what it binds, not its row id
|
|
31
|
+
* authority). A binding row's identity is what it binds, not its row id, and a
|
|
32
|
+
* row the no-edit recompile minted only for nodes merge kept from the original
|
|
33
|
+
* is not carried (see withoutRowsMintedForKeptNodes). Node ids are stable by construction (decompile names every step by
|
|
32
34
|
* its node id), which is what lets every re-attach key on id.
|
|
33
35
|
*
|
|
34
36
|
* ## Definitions are keyed by `nodeType@version`, not by `nodeType`
|
package/dist/merge.js
CHANGED
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
* preserved from the original; `subflows` and edges (with their ids) come from
|
|
29
29
|
* `edited`; `definitions` and `bindings` are unioned
|
|
30
30
|
* (original wins on conflict, since the original's are the tenant-resolved
|
|
31
|
-
* authority). A binding row's identity is what it binds, not its row id
|
|
31
|
+
* authority). A binding row's identity is what it binds, not its row id, and a
|
|
32
|
+
* row the no-edit recompile minted only for nodes merge kept from the original
|
|
33
|
+
* is not carried (see withoutRowsMintedForKeptNodes). Node ids are stable by construction (decompile names every step by
|
|
32
34
|
* its node id), which is what lets every re-attach key on id.
|
|
33
35
|
*
|
|
34
36
|
* ## Definitions are keyed by `nodeType@version`, not by `nodeType`
|
|
@@ -212,12 +214,17 @@ function mergeFlowFindings(original, edited, options) {
|
|
|
212
214
|
// ── Nodes: the edited graph decides which nodes exist and in what order; the
|
|
213
215
|
// original decides their look (and, for untouched nodes, their content). ──
|
|
214
216
|
const finalNodes = [];
|
|
217
|
+
// The edited nodes the merge replaced with the original's: their bindings
|
|
218
|
+
// references are the original's now, not the recompile's
|
|
219
|
+
// (see withoutRowsMintedForKeptNodes).
|
|
220
|
+
const keptFromOriginal = new Set();
|
|
215
221
|
for (const e of edited.nodes ?? []) {
|
|
216
222
|
const orig = origNodes.get(e.id);
|
|
217
223
|
if (isKept(e, origNodes, baseNodes, options.baseline) && orig) {
|
|
218
224
|
// Untouched by the user (equal to the faithful baseline) → keep the
|
|
219
225
|
// original node exactly, placeholder-safe.
|
|
220
226
|
finalNodes.push(jsonClone(orig));
|
|
227
|
+
keptFromOriginal.add(e.id);
|
|
221
228
|
}
|
|
222
229
|
else {
|
|
223
230
|
// Added or edited → take the edited node, re-attaching original layout.
|
|
@@ -266,9 +273,11 @@ function mergeFlowFindings(original, edited, options) {
|
|
|
266
273
|
];
|
|
267
274
|
result.definitions = mergeDefinitions(original.definitions, edited.definitions, allNodes, originalNodes, options.warn);
|
|
268
275
|
// ── Bindings: union by what each row binds (original wins), so resources for
|
|
269
|
-
// newly-added nodes come from `edited` while the original's stay intact.
|
|
276
|
+
// newly-added nodes come from `edited` while the original's stay intact.
|
|
277
|
+
// A row the no-edit recompile minted only for nodes the merge kept from the
|
|
278
|
+
// original is not carried (#1133). ──
|
|
270
279
|
result.bindings = mergeBindings(original.bindings, edited.bindings);
|
|
271
|
-
result.bindings =
|
|
280
|
+
result.bindings = withoutRowsMintedForKeptNodes(result.bindings, original, edited, options.baseline, keptFromOriginal);
|
|
272
281
|
// metadata / solutionId / projectId / layout / runtime were carried by the
|
|
273
282
|
// initial clone of `original` and are intentionally left untouched.
|
|
274
283
|
//
|
|
@@ -379,12 +388,14 @@ function mergeDefinitions(orig, edited, nodes, originalNodes, warn) {
|
|
|
379
388
|
* holds it, in which case it gets a fresh one — ids are unreferenced labels, but
|
|
380
389
|
* two rows with one id are not a valid file.
|
|
381
390
|
*
|
|
382
|
-
* Rows
|
|
391
|
+
* Rows of the ORIGINAL are never pruned. The canvas file has no uniform
|
|
383
392
|
* node-to-row reference to decide "unused" by: an in-solution Flow is named by
|
|
384
393
|
* the node TYPE's suffix, a connection by an id in the node's inputs, a published
|
|
385
394
|
* process by a folder-qualified key that no node field spells. A wrong prune
|
|
386
395
|
* drops a row a kept node needs, and the run fails to resolve its resource; an
|
|
387
|
-
* orphan row is accepted by `validate` and ignored at run time.
|
|
396
|
+
* orphan row is accepted by `validate` and ignored at run time. Added rows the
|
|
397
|
+
* round trip minted only for kept nodes are dropped afterwards, by
|
|
398
|
+
* {@link withoutRowsMintedForKeptNodes}.
|
|
388
399
|
*/
|
|
389
400
|
function mergeBindings(orig, edited) {
|
|
390
401
|
const out = (orig ?? []).map((b) => jsonClone(b));
|
|
@@ -409,41 +420,70 @@ function mergeBindings(orig, edited) {
|
|
|
409
420
|
return out;
|
|
410
421
|
}
|
|
411
422
|
/**
|
|
412
|
-
* Drop each added row that the no-edit recompile minted for
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
423
|
+
* Drop each added row that the no-edit recompile minted only for nodes the merge
|
|
424
|
+
* kept from the original (#1133). A row qualifies when the original lacks it, the
|
|
425
|
+
* baseline has it, and at least one edited node references it, every one of
|
|
426
|
+
* which is in `keptFromOriginal`: the kept node is the original's, which reaches
|
|
427
|
+
* its resource without that row.
|
|
417
428
|
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
429
|
+
* Examples:
|
|
430
|
+
* - A classic-CLI IxP extract carries its model inline and has no rows. It
|
|
431
|
+
* decompiles to `ixpExtract(...)`, whose compile mints `ixpDeployment` rows
|
|
432
|
+
* keyed by the model id.
|
|
433
|
+
* - A classic-CLI `core.action.http.v2` with no connection decompiles to
|
|
434
|
+
* `http({ managed: true })`, whose compile mints `""`-keyed rows for
|
|
435
|
+
* `<bindings.uipath-uipath-http connection>` and `<bindings.FolderKey>`.
|
|
422
436
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
437
|
+
* "References" is read off the EDITED file, which the SDK wrote. A node
|
|
438
|
+
* references a row when it spells the row's `resourceKey`, or when it spells the
|
|
439
|
+
* row's `<bindings.NAME>` placeholder (in the node, or in its type's definition)
|
|
440
|
+
* and points at that row's resource. Names are not unique: a Slack step on
|
|
441
|
+
* connection X and a connection-less http step both use `<bindings.FolderKey>`,
|
|
442
|
+
* each with its own `FolderKey` row. So a placeholder counts for a keyed row
|
|
443
|
+
* only when no other row has that name, and for a `""`-keyed row only when the
|
|
444
|
+
* node spells none of the other same-named rows' keys.
|
|
445
|
+
*
|
|
446
|
+
* Kept: a row no edited node references (one a sidecar file uses, say), a row
|
|
447
|
+
* a subflow node or an edited node references, and every row without a baseline.
|
|
425
448
|
*/
|
|
426
|
-
function
|
|
427
|
-
if (!baseline || !rows)
|
|
449
|
+
function withoutRowsMintedForKeptNodes(rows, original, edited, baseline, keptFromOriginal) {
|
|
450
|
+
if (!baseline || !rows || keptFromOriginal.size === 0)
|
|
428
451
|
return rows;
|
|
429
|
-
const
|
|
430
|
-
const
|
|
452
|
+
const contextByType = new Map();
|
|
453
|
+
for (const d of (edited.definitions ?? [])) {
|
|
454
|
+
if (typeof d?.nodeType !== 'string' || d.model?.context === undefined)
|
|
455
|
+
continue;
|
|
456
|
+
contextByType.set(d.nodeType, (contextByType.get(d.nodeType) ?? '') + JSON.stringify(d.model.context));
|
|
457
|
+
}
|
|
458
|
+
const describe = (n, kept) => ({ kept, text: JSON.stringify(n) + (contextByType.get(n.type) ?? '') });
|
|
431
459
|
const nodes = [
|
|
432
|
-
...(edited.nodes ?? []).map((n) => (
|
|
433
|
-
...Object.values(edited.subflows ?? {}).flatMap((sf) => (sf?.nodes ?? []).map((n) => (
|
|
460
|
+
...(edited.nodes ?? []).map((n) => describe(n, keptFromOriginal.has(n.id))),
|
|
461
|
+
...Object.values(edited.subflows ?? {}).flatMap((sf) => (sf?.nodes ?? []).map((n) => describe(n, false))),
|
|
434
462
|
];
|
|
435
|
-
|
|
436
|
-
|
|
463
|
+
const spelled = (s) => JSON.stringify(s).slice(1, -1);
|
|
464
|
+
const keyOf = (b) => (typeof b.resourceKey === 'string' ? b.resourceKey : '');
|
|
465
|
+
const all = (edited.bindings ?? []);
|
|
437
466
|
const fromOriginal = new Set((original.bindings ?? []).map(bindingIdentity));
|
|
438
467
|
const inBaseline = new Set((baseline.bindings ?? []).map(bindingIdentity));
|
|
439
468
|
return rows.filter((b) => {
|
|
440
469
|
const identity = bindingIdentity(b);
|
|
441
470
|
if (fromOriginal.has(identity) || !inBaseline.has(identity))
|
|
442
471
|
return true;
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
472
|
+
const key = keyOf(b);
|
|
473
|
+
const others = typeof b.name === 'string'
|
|
474
|
+
? all.filter((o) => o.name === b.name && keyOf(o) !== key)
|
|
475
|
+
: [];
|
|
476
|
+
const placeholder = typeof b.name === 'string' ? spelled(`<bindings.${b.name}>`) : undefined;
|
|
477
|
+
const references = (text) => {
|
|
478
|
+
if (key !== '' && text.includes(spelled(key)))
|
|
479
|
+
return true;
|
|
480
|
+
if (placeholder === undefined || !text.includes(placeholder))
|
|
481
|
+
return false;
|
|
482
|
+
return key === ''
|
|
483
|
+
? !others.some((o) => text.includes(spelled(keyOf(o))))
|
|
484
|
+
: others.length === 0;
|
|
485
|
+
};
|
|
486
|
+
const referencers = nodes.filter((n) => references(n.text));
|
|
487
|
+
return !(referencers.length > 0 && referencers.every((n) => n.kept));
|
|
448
488
|
});
|
|
449
489
|
}
|
|
@@ -7,3 +7,11 @@
|
|
|
7
7
|
* nothing.
|
|
8
8
|
*/
|
|
9
9
|
export declare function resourceSourcesOfOtherFlows(dir: string, self: string): Map<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Every `inputs.source` string a flow's nodes name — agent folders and agent
|
|
12
|
+
* resource ids alike — root scope and subflow bodies, in the case the file
|
|
13
|
+
* spells them. A malformed flow yields nothing. The one walk both
|
|
14
|
+
* {@link resourceSourcesOfOtherFlows} and compile's own-flow checks use, so the
|
|
15
|
+
* two cannot disagree on where a node keeps its source.
|
|
16
|
+
*/
|
|
17
|
+
export declare function flowSources(flow: unknown): string[];
|
|
@@ -31,17 +31,31 @@ export function resourceSourcesOfOtherFlows(dir, self) {
|
|
|
31
31
|
catch {
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const src = n?.inputs?.source;
|
|
38
|
-
if (typeof src === 'string' && !users.has(src))
|
|
39
|
-
users.set(src, f);
|
|
40
|
-
}
|
|
41
|
-
if (s?.subflows && typeof s.subflows === 'object')
|
|
42
|
-
Object.values(s.subflows).forEach(walk);
|
|
43
|
-
};
|
|
44
|
-
walk(flow);
|
|
34
|
+
for (const src of flowSources(flow))
|
|
35
|
+
if (!users.has(src))
|
|
36
|
+
users.set(src, f);
|
|
45
37
|
}
|
|
46
38
|
return users;
|
|
47
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Every `inputs.source` string a flow's nodes name — agent folders and agent
|
|
42
|
+
* resource ids alike — root scope and subflow bodies, in the case the file
|
|
43
|
+
* spells them. A malformed flow yields nothing. The one walk both
|
|
44
|
+
* {@link resourceSourcesOfOtherFlows} and compile's own-flow checks use, so the
|
|
45
|
+
* two cannot disagree on where a node keeps its source.
|
|
46
|
+
*/
|
|
47
|
+
export function flowSources(flow) {
|
|
48
|
+
const ids = [];
|
|
49
|
+
const walk = (scope) => {
|
|
50
|
+
const s = scope;
|
|
51
|
+
for (const n of Array.isArray(s?.nodes) ? s.nodes : []) {
|
|
52
|
+
const src = n?.inputs?.source;
|
|
53
|
+
if (typeof src === 'string')
|
|
54
|
+
ids.push(src);
|
|
55
|
+
}
|
|
56
|
+
if (s?.subflows && typeof s.subflows === 'object')
|
|
57
|
+
Object.values(s.subflows).forEach(walk);
|
|
58
|
+
};
|
|
59
|
+
walk(flow);
|
|
60
|
+
return ids;
|
|
61
|
+
}
|
package/dist/serialize.d.ts
CHANGED
|
@@ -99,6 +99,18 @@ export interface SerializeOpts {
|
|
|
99
99
|
path: string;
|
|
100
100
|
json: unknown;
|
|
101
101
|
}>;
|
|
102
|
+
/**
|
|
103
|
+
* Maps an agent's folder uuid (its `source`) to the one this compile writes,
|
|
104
|
+
* given the step's name. `compile-cli` passes it when the `source` a step
|
|
105
|
+
* carries already belongs to ANOTHER `.flow` in the same Flow project (#1184):
|
|
106
|
+
* decompile keeps a canvas agent's `source`, so compiling that source to a
|
|
107
|
+
* second `.flow` would otherwise write into the first flow's folder. The uuid
|
|
108
|
+
* returned is spent everywhere the `source` is: the node's `inputs.source`,
|
|
109
|
+
* the `agent.json` identity, and every sidecar path.
|
|
110
|
+
*
|
|
111
|
+
* @internal
|
|
112
|
+
*/
|
|
113
|
+
agentSource?: (source: string, step: string) => string;
|
|
102
114
|
}
|
|
103
115
|
/**
|
|
104
116
|
* `serialize`, plus the extra files the flow needs on disk beside it.
|
package/dist/serialize.js
CHANGED
|
@@ -4335,6 +4335,17 @@ export function serialize(built, opts = {}) {
|
|
|
4335
4335
|
* gets them and one that does not is unaffected.
|
|
4336
4336
|
*/
|
|
4337
4337
|
const sidecars = opts.sidecars ?? [];
|
|
4338
|
+
/**
|
|
4339
|
+
* An agent step's folder uuid (`source`): the one the step carries, else one
|
|
4340
|
+
* derived from the flow id + step name rather than random, so a recompile is
|
|
4341
|
+
* byte-identical — a v4-shaped uuid over a stable hash, because the platform
|
|
4342
|
+
* treats it as an opaque id and the designer shows it as a folder. Then
|
|
4343
|
+
* `opts.agentSource` may move it to a folder this flow owns (#1184).
|
|
4344
|
+
*/
|
|
4345
|
+
const agentFolder = (carried, stepName) => {
|
|
4346
|
+
const source = carried ?? fnv1aX4Uuid(`${built.id}/${stepName}`);
|
|
4347
|
+
return opts.agentSource ? opts.agentSource(source, stepName) : source;
|
|
4348
|
+
};
|
|
4338
4349
|
const subflows = {};
|
|
4339
4350
|
const rootGlobals = emittableGlobals(built, `flow "${built.id}"`);
|
|
4340
4351
|
const root = newScope(rootGlobals.outputs, rootGlobals.vars, rootGlobals.backing, rootGlobals.backingReads);
|
|
@@ -5146,11 +5157,8 @@ export function serialize(built, opts = {}) {
|
|
|
5146
5157
|
: `Drop { version: '${version}' } to use ${INLINE_AGENT_DEFAULT_VERSION}, or drop \`memory\`.`));
|
|
5147
5158
|
}
|
|
5148
5159
|
const id = stepUid(scope, step.name);
|
|
5149
|
-
// `source` is the agent's directory name
|
|
5150
|
-
|
|
5151
|
-
// over a stable hash, because the platform treats it as an opaque id and the
|
|
5152
|
-
// designer shows it as a folder.
|
|
5153
|
-
const source = s.source ?? fnv1aX4Uuid(`${built.id}/${step.name}`);
|
|
5160
|
+
// `source` is the agent's directory name; see `agentFolder`.
|
|
5161
|
+
const source = agentFolder(s.source, step.name);
|
|
5154
5162
|
const node = makeNode(def.nodeType, id, step.name, { parentId, requestedVersion: step.options?.version, def });
|
|
5155
5163
|
// Unlike the published families the inline agent KEEPS no node-level model:
|
|
5156
5164
|
// 29 of 29 deployed instances carry none (the def's `model.source: true` is
|
|
@@ -5706,7 +5714,7 @@ export function serialize(built, opts = {}) {
|
|
|
5706
5714
|
// lets the platform do that translation.
|
|
5707
5715
|
const s = spec.inputs;
|
|
5708
5716
|
const id = stepUid(scope, step.name);
|
|
5709
|
-
const source = s.source
|
|
5717
|
+
const source = agentFolder(s.source, step.name);
|
|
5710
5718
|
const node = makeNode(NODE_TYPE.conversationalAgent, id, step.name, { parentId, requestedVersion: step.options?.version });
|
|
5711
5719
|
delete node.model;
|
|
5712
5720
|
const st = s.settings ?? {};
|
|
@@ -5799,7 +5807,7 @@ export function serialize(built, opts = {}) {
|
|
|
5799
5807
|
// definition declares what comes back.
|
|
5800
5808
|
const s = spec.inputs;
|
|
5801
5809
|
const id = stepUid(scope, step.name);
|
|
5802
|
-
const source = s.source
|
|
5810
|
+
const source = agentFolder(s.source, step.name);
|
|
5803
5811
|
const node = makeNode(NODE_TYPE.voiceAgent, id, step.name, { parentId, requestedVersion: step.options?.version });
|
|
5804
5812
|
// Same rule as the inline agent's: the resource model lives on the
|
|
5805
5813
|
// 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.31.
|
|
3
|
+
"version": "6.31.6",
|
|
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",
|
|
@@ -93,5 +93,5 @@
|
|
|
93
93
|
"@types/node": "^22.7.0",
|
|
94
94
|
"esbuild": "^0.28.1"
|
|
95
95
|
},
|
|
96
|
-
"gitref": "
|
|
96
|
+
"gitref": "77c2e7b1f09268cc94ee0f4b8f9d4ae80ba625ee"
|
|
97
97
|
}
|