@uipath/maestro-builder-sdk 6.11.6 → 6.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-index.md +83 -56
- package/dist/api-members.md +362 -261
- package/dist/bpmn/bpmn-expr-check.js +8 -1
- package/dist/bpmn/bpmn-sdk.d.ts +1026 -27
- package/dist/bpmn/bpmn-sdk.js +1328 -83
- package/dist/bpmn/check-cli.js +19 -4
- package/dist/bpmn/check.d.ts +1 -1
- package/dist/bpmn/check.js +529 -13
- package/dist/bpmn/connector.d.ts +2 -2
- package/dist/bpmn/connector.js +74 -37
- package/dist/bpmn/data-service.d.ts +124 -0
- package/dist/bpmn/data-service.js +98 -0
- package/dist/bpmn/decompile-cli.js +70 -8
- package/dist/bpmn/decompile.d.ts +71 -0
- package/dist/bpmn/decompile.js +845 -31
- package/dist/bpmn/graph.d.ts +69 -0
- package/dist/bpmn/graph.js +98 -0
- package/dist/bpmn/intsvc.js +2 -2
- package/dist/bpmn/known-methods.d.ts +15 -0
- package/dist/bpmn/known-methods.js +271 -0
- package/dist/bpmn/lift-verify.d.ts +40 -0
- package/dist/bpmn/lift-verify.js +133 -0
- package/dist/bpmn/serialize.d.ts +65 -1
- package/dist/bpmn/serialize.js +169 -22
- package/dist/bpmn/typed-node.d.ts +37 -6
- package/dist/bpmn/typed-node.js +81 -19
- package/dist/cli/commands/bpmn.js +7 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -0
- package/package.json +2 -2
package/dist/bpmn/decompile.js
CHANGED
|
@@ -60,6 +60,12 @@ import { BPMN_SCHEMA_VERSIONS } from './format-profile.js';
|
|
|
60
60
|
import { preserveSchemaVersion } from '../schema-version.js';
|
|
61
61
|
const require = createRequire(import.meta.url);
|
|
62
62
|
const uipathDescriptor = require('./uipath-moddle.v1.json');
|
|
63
|
+
/**
|
|
64
|
+
* Set while the nested style writes a sequence-mode root: a sub-process body the
|
|
65
|
+
* flat renderer writes inherits the mode, and its trailing `.sequenceFlow()`s would
|
|
66
|
+
* then double every edge, so the body pins `.flowMode('explicit')` for itself.
|
|
67
|
+
*/
|
|
68
|
+
let pinExplicitInBodies = false;
|
|
63
69
|
// ─── Source emitters ──────────────────────────────────────────────────────────
|
|
64
70
|
/**
|
|
65
71
|
* A JS/TS string literal.
|
|
@@ -203,6 +209,15 @@ class Chain {
|
|
|
203
209
|
add(call) {
|
|
204
210
|
this.lines.push(`${this.indent}${call}`);
|
|
205
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Put a call at the FRONT of the chain.
|
|
214
|
+
*
|
|
215
|
+
* For the declarations that belong at the top but are only known once the body has
|
|
216
|
+
* been walked — `.flowIds()`, which lists the implied edges the walk decided to pin.
|
|
217
|
+
*/
|
|
218
|
+
addFirst(call) {
|
|
219
|
+
this.lines.unshift(`${this.indent}${call}`);
|
|
220
|
+
}
|
|
206
221
|
get all() {
|
|
207
222
|
return this.lines;
|
|
208
223
|
}
|
|
@@ -463,14 +478,23 @@ function emitTypedNode(chain, el, payload, type) {
|
|
|
463
478
|
const ctx = contextOf(payload);
|
|
464
479
|
const inputRows = payload.input ?? [];
|
|
465
480
|
let inputs;
|
|
481
|
+
let verbatimInputs;
|
|
466
482
|
if (spec.inputPattern === 'separateInputs') {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
483
|
+
if (inputRowsNeedSpellingOut(inputRows, spec)) {
|
|
484
|
+
// A row the record cannot reproduce — a connector parameter whose `target` is
|
|
485
|
+
// `query` rather than its own name, a type the registry does not declare — is
|
|
486
|
+
// spelled out as `inputRows`, the way context and output rows are.
|
|
487
|
+
verbatimInputs = emitInputRows(inputRows);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
// A row's value is in its BODY or its `value` attribute, and which one is
|
|
491
|
+
// decided by the field's declared type — see `payloadInput` in typed-node.ts.
|
|
492
|
+
// Reading only the body silently produced `{}` for every scalar row, so a
|
|
493
|
+
// string correlation reference round-tripped to `value="[object Object]"`.
|
|
494
|
+
// Nothing before this authored a separateInputs type with a declared scalar
|
|
495
|
+
// field, so every row really was a body and the bug had nowhere to appear.
|
|
496
|
+
inputs = Object.fromEntries(inputRows.map((r) => [r.name, r.body !== undefined ? safeJson(r.body) : r.value]));
|
|
497
|
+
}
|
|
474
498
|
}
|
|
475
499
|
else if (inputRows.length) {
|
|
476
500
|
inputs = safeJson(inputRows[0].body);
|
|
@@ -488,6 +512,7 @@ function emitTypedNode(chain, el, payload, type) {
|
|
|
488
512
|
['contextRows', verbatimContext],
|
|
489
513
|
['inputSchema', payload.context?.inputSchema?.body !== undefined ? jsonLiteral(payload.context.inputSchema.body) : undefined],
|
|
490
514
|
['inputs', record(inputs)],
|
|
515
|
+
['inputRows', verbatimInputs],
|
|
491
516
|
['outputRows', verbatimRows],
|
|
492
517
|
['outputVar', verbatimRows === undefined && outputVar !== undefined && outputVar !== defaultVar ? str(outputVar) : undefined],
|
|
493
518
|
['outputs', verbatimRows === undefined ? record(extras) : undefined],
|
|
@@ -499,6 +524,70 @@ function emitTypedNode(chain, el, payload, type) {
|
|
|
499
524
|
['loop', emitLoop(el)],
|
|
500
525
|
]))})`);
|
|
501
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Whether `inputs` — a name→value record — would reproduce these `separateInputs`
|
|
529
|
+
* rows, or whether they need spelling out as `inputRows`.
|
|
530
|
+
*
|
|
531
|
+
* The record derives a row's target and type (the registry's declaration, else the
|
|
532
|
+
* key and `json`-for-structured / `string`-for-scalar — see `inputRows` in
|
|
533
|
+
* typed-node.ts), so a row is only reproducible when it carries exactly those. A
|
|
534
|
+
* connector's flat parameter rows never do: `name="start" … target="query"`.
|
|
535
|
+
*/
|
|
536
|
+
function inputRowsNeedSpellingOut(rows, spec) {
|
|
537
|
+
const declared = new Map(spec.inputs.map((f) => [f.name, f]));
|
|
538
|
+
return rows.some((r) => {
|
|
539
|
+
const field = declared.get(r.name);
|
|
540
|
+
if (r.value === undefined && r.body === undefined)
|
|
541
|
+
return true;
|
|
542
|
+
if (r.target !== (field?.target ?? r.name))
|
|
543
|
+
return true;
|
|
544
|
+
const derivedType = field?.type ?? (r.body !== undefined ? 'json' : 'string');
|
|
545
|
+
return (r.type ?? 'string') !== derivedType;
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
/** The payload rows as `TypedInputRow` option source, in document order. */
|
|
549
|
+
function emitInputRows(rows) {
|
|
550
|
+
if (!rows.length)
|
|
551
|
+
return undefined;
|
|
552
|
+
return `[${rows
|
|
553
|
+
.map((r) => opts([
|
|
554
|
+
['name', str(r.name)],
|
|
555
|
+
['type', r.type !== undefined ? str(r.type) : undefined],
|
|
556
|
+
['target', r.target !== undefined ? str(r.target) : undefined],
|
|
557
|
+
['value', r.value !== undefined ? str(r.value) : undefined],
|
|
558
|
+
['body', r.body !== undefined ? (r.type === 'json' ? jsonLiteral(r.body) : str(String(r.body))) : undefined],
|
|
559
|
+
]))
|
|
560
|
+
.join(', ')}]`;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* The serializer's default scope for a public declaration, read off the artifact —
|
|
564
|
+
* the mirror of `publicDeclarationScope` in serialize.ts, so a declaration the
|
|
565
|
+
* platform scopes the way the SDK would is emitted without an `elementId`.
|
|
566
|
+
*/
|
|
567
|
+
function publicDeclarationScopeOf(process, processId) {
|
|
568
|
+
const elements = process.flowElements ?? [];
|
|
569
|
+
// "Manual" must mean the same thing here as in the emitter, which asks
|
|
570
|
+
// `n.event === undefined && n.trigger === undefined`. Excluding only a `uipath:Event`
|
|
571
|
+
// payload did not match it: a trigger start is emitted through its REGISTRY spec, and
|
|
572
|
+
// `Intsvc.TimerTrigger`'s tag is `uipath:activity`, so a trigger with no timer came
|
|
573
|
+
// back as a `bpmn:StartEvent` with empty `eventDefinitions` and a `uipath:Activity`
|
|
574
|
+
// payload — manual to this side, not manual to the emitter. The round trip then moved
|
|
575
|
+
// a public input's `elementId` from the trigger start to the process. Excluding ANY
|
|
576
|
+
// typed payload makes the two agree for every shape the SDK can emit.
|
|
577
|
+
const manualStarts = elements.filter((el) => el.$type === 'bpmn:StartEvent' && !(el.eventDefinitions ?? []).length && payloadOf(el) === undefined);
|
|
578
|
+
const ends = elements.filter((el) => el.$type === 'bpmn:EndEvent');
|
|
579
|
+
return (kind, v) => {
|
|
580
|
+
if (kind === 'input')
|
|
581
|
+
return manualStarts.length === 1 ? manualStarts[0].id : processId;
|
|
582
|
+
if (kind === 'output') {
|
|
583
|
+
const writers = ends.filter((el) => (payloadOf(el)?.output ?? []).some((row) => row.var === v.id));
|
|
584
|
+
if (writers.length === 1)
|
|
585
|
+
return writers[0].id;
|
|
586
|
+
return ends.length === 1 ? ends[0].id : processId;
|
|
587
|
+
}
|
|
588
|
+
return processId;
|
|
589
|
+
};
|
|
590
|
+
}
|
|
502
591
|
/** Mirrors the emitter's own default-variable naming, so a default is not echoed. */
|
|
503
592
|
function identifierize(name) {
|
|
504
593
|
const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
@@ -521,6 +610,15 @@ function safeJson(body) {
|
|
|
521
610
|
return body;
|
|
522
611
|
}
|
|
523
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* The `uipath:scriptVersion` a script task carries — `'v1'` when it carries none,
|
|
615
|
+
* because that is what an absent element means to the runtime (`IsVersionAtLeast`
|
|
616
|
+
* treats it as the old, spread-result contract).
|
|
617
|
+
*/
|
|
618
|
+
function scriptVersionOf(el) {
|
|
619
|
+
const sv = (el.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:ScriptVersion');
|
|
620
|
+
return typeof sv?.value === 'string' && sv.value.trim() !== '' ? sv.value : 'v1';
|
|
621
|
+
}
|
|
524
622
|
/** A `uipath:mapping` script task. */
|
|
525
623
|
function emitScriptTask(chain, el, payload, type) {
|
|
526
624
|
const args = (payload?.input ?? []).find((r) => r.name === 'args');
|
|
@@ -537,6 +635,10 @@ function emitScriptTask(chain, el, payload, type) {
|
|
|
537
635
|
['name', el.name !== undefined ? str(el.name) : undefined],
|
|
538
636
|
['script', str(el.script ?? '')],
|
|
539
637
|
['scriptFormat', el.scriptFormat && el.scriptFormat !== 'JavaScript' ? str(el.scriptFormat) : undefined],
|
|
638
|
+
// The builder's default is v3; anything else is load-bearing (it selects the
|
|
639
|
+
// runtime's result shape) and must survive the round trip. An absent element
|
|
640
|
+
// is spelled `'v1'`, the meaning it has to the runtime.
|
|
641
|
+
['scriptVersion', scriptVersionOf(el) !== 'v3' ? str(scriptVersionOf(el)) : undefined],
|
|
540
642
|
// Only when it is neither the contract's value nor the invalid one this SDK
|
|
541
643
|
// used to emit. `BPMN.ScriptTask` is NORMALISED AWAY rather than preserved:
|
|
542
644
|
// the runtime contract has no such type, so reproducing it would propagate a
|
|
@@ -594,6 +696,666 @@ function emitScope(chain, container, depth, indent) {
|
|
|
594
696
|
]))})`);
|
|
595
697
|
}
|
|
596
698
|
}
|
|
699
|
+
// ─── The lift: flat regions → the nesting constructs (`style: 'nested'`) ───────
|
|
700
|
+
//
|
|
701
|
+
// Every construct the builder offers lowers to plain nodes and flows, so a flat
|
|
702
|
+
// graph can be read back into the construct wherever the pattern holds exactly.
|
|
703
|
+
// The rules below are deliberately conservative — each one names a shape the
|
|
704
|
+
// construct is KNOWN to lower to — and `decompile-cli` still builds both sources
|
|
705
|
+
// and compares the graphs before it writes the nested one. Anything the rules
|
|
706
|
+
// cannot prove stays flat, in the same file, next to what was lifted.
|
|
707
|
+
const LIFT_GATEWAYS = new Set(['exclusiveGateway', 'inclusiveGateway', 'parallelGateway', 'eventBasedGateway']);
|
|
708
|
+
function scopeGraph(container) {
|
|
709
|
+
const g = { elements: [], byId: new Map(), flows: [], outs: new Map(), ins: new Map() };
|
|
710
|
+
for (const el of container.flowElements ?? []) {
|
|
711
|
+
if (el.$type === 'bpmn:SequenceFlow') {
|
|
712
|
+
g.flows.push(el);
|
|
713
|
+
const s = el.sourceRef?.id;
|
|
714
|
+
const t = el.targetRef?.id;
|
|
715
|
+
if (s !== undefined)
|
|
716
|
+
g.outs.set(s, [...(g.outs.get(s) ?? []), el]);
|
|
717
|
+
if (t !== undefined)
|
|
718
|
+
g.ins.set(t, [...(g.ins.get(t) ?? []), el]);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
g.elements.push(el);
|
|
722
|
+
g.byId.set(el.id, el);
|
|
723
|
+
}
|
|
724
|
+
return g;
|
|
725
|
+
}
|
|
726
|
+
/** The id the builder derives for an implied edge between a flow's ends. */
|
|
727
|
+
function derivedFlowId(f) {
|
|
728
|
+
return `Flow_${f.sourceRef?.id}_${f.targetRef?.id}`;
|
|
729
|
+
}
|
|
730
|
+
/** Whether a flow's id is the one the builder derives, so an implied edge reproduces it. */
|
|
731
|
+
function derivableFlow(f) {
|
|
732
|
+
return f.id === derivedFlowId(f);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Consume a flow into a construct: it is no longer written as `.sequenceFlow()`, and
|
|
736
|
+
* when its id is not the derived one the recompile renames it — recorded on the
|
|
737
|
+
* summary so the CLI can verify the graphs modulo exactly those ids and report them.
|
|
738
|
+
*/
|
|
739
|
+
function implyFlow(plan, f) {
|
|
740
|
+
plan.impliedFlows.add(f.id);
|
|
741
|
+
if (derivableFlow(f))
|
|
742
|
+
return;
|
|
743
|
+
// The id is not the one the construct derives. PIN it rather than let the recompile
|
|
744
|
+
// rename it: `bpmn merge` keys every element by id, flows included, so a renamed flow
|
|
745
|
+
// reads as a new one and the original — with its diagram edge — is deleted, and any
|
|
746
|
+
// gateway `default` preserved from the original is left naming an id that is gone.
|
|
747
|
+
// `.flowIds()` keeps the edge implied and gives it the id it came with.
|
|
748
|
+
if (plan.relabel)
|
|
749
|
+
plan.pinned.push({ source: f.sourceRef?.id, target: f.targetRef?.id, id: f.id });
|
|
750
|
+
else
|
|
751
|
+
plan.summary.renamedFlows.push({ from: f.id, to: derivedFlowId(f) });
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* The first flow of an arm that carries a label the construct cannot keep, or
|
|
755
|
+
* undefined. An implied edge has no `name`; only a `choose` arm's ENTRY flow keeps its
|
|
756
|
+
* label (as `label`). An empty `name=""` — designer residue on many imports — counts
|
|
757
|
+
* as no label, and is dropped on a relabelled edge.
|
|
758
|
+
*/
|
|
759
|
+
function labelledImpliedFlow(flows, entryKeepsLabel, kept = new Set()) {
|
|
760
|
+
return flows.find((f, i) => typeof f.name === 'string' && f.name !== '' && !(i === 0 && entryKeepsLabel) && !kept.has(f.id));
|
|
761
|
+
}
|
|
762
|
+
/** The flows a lift keeps the label of — every `choose` arm's entry flow, nested constructs' included. */
|
|
763
|
+
function labelKeptFlows(lift, into = new Set()) {
|
|
764
|
+
for (const a of lift.arms) {
|
|
765
|
+
if (lift.construct === 'choose')
|
|
766
|
+
into.add(a.flow.id);
|
|
767
|
+
for (const item of a.arm.items)
|
|
768
|
+
if ('lift' in item)
|
|
769
|
+
labelKeptFlows(item.lift, into);
|
|
770
|
+
}
|
|
771
|
+
return into;
|
|
772
|
+
}
|
|
773
|
+
function isEventSubProcess(el) {
|
|
774
|
+
return el.$type === 'bpmn:SubProcess' && el.triggeredByEvent === true;
|
|
775
|
+
}
|
|
776
|
+
/** An element an arm may contain: something with one way in and one way out, or an end. */
|
|
777
|
+
function isArmNode(el) {
|
|
778
|
+
const kind = localName(el.$type);
|
|
779
|
+
return !LIFT_GATEWAYS.has(kind) && kind !== 'boundaryEvent' && kind !== 'startEvent' && !isEventSubProcess(el);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Follow one path from `entry` as far as an arm can own it.
|
|
783
|
+
*
|
|
784
|
+
* The arm owns a chain of elements each with exactly one incoming flow (the
|
|
785
|
+
* previous one) and exactly one outgoing, and it ends at an end event, at a
|
|
786
|
+
* `joinOk` gateway, or — when the path reaches something it cannot own (a merge
|
|
787
|
+
* point, a consumed element, an element that fans out) — by jumping there with
|
|
788
|
+
* `.goto()`. A gateway the path alone leads into is lifted INSIDE the arm as a
|
|
789
|
+
* nested construct, and the arm carries on from that construct's exit when it has
|
|
790
|
+
* one. Every flow it implies must carry the derived id unless the lift may relabel.
|
|
791
|
+
*
|
|
792
|
+
* `taken` is shared with the caller and grows as the arm claims elements, so a
|
|
793
|
+
* nested construct's arms and the arms that follow see what this one owns.
|
|
794
|
+
*/
|
|
795
|
+
function walkArm(plan, entry, taken, joinOk, emitted) {
|
|
796
|
+
const { g, relabel } = plan;
|
|
797
|
+
const fail = (reason) => ({ ok: false, reason, elements: [], items: [], flows: [], labelKept: new Set(), end: 'terminal' });
|
|
798
|
+
const elements = [];
|
|
799
|
+
const items = [];
|
|
800
|
+
const flows = [];
|
|
801
|
+
const labelKept = new Set();
|
|
802
|
+
const done = (end, extra = {}) => ({ ok: true, elements, items, flows, labelKept, end, ...extra });
|
|
803
|
+
let f = entry;
|
|
804
|
+
let tgt;
|
|
805
|
+
for (let guard = 0; guard < 10_000; guard++) {
|
|
806
|
+
if (f !== undefined) {
|
|
807
|
+
if (!relabel && !derivableFlow(f))
|
|
808
|
+
return fail(`flow "${f.id}" is not named Flow_<source>_<target>, which an implied edge would be`);
|
|
809
|
+
flows.push(f);
|
|
810
|
+
tgt = g.byId.get(f.targetRef?.id);
|
|
811
|
+
if (!tgt)
|
|
812
|
+
return fail(`flow "${f.id}" leaves the scope`);
|
|
813
|
+
}
|
|
814
|
+
if (!tgt)
|
|
815
|
+
return fail('the path does not end');
|
|
816
|
+
const incoming = (g.ins.get(tgt.id) ?? []).length;
|
|
817
|
+
// A join MERGES: a same-kind gateway only this path leads into is not the
|
|
818
|
+
// construct's join but a decision of its own, lifted inside the arm below.
|
|
819
|
+
if (joinOk(tgt) && incoming > 1)
|
|
820
|
+
return done('join', { join: tgt });
|
|
821
|
+
if (incoming > 1 || taken.has(tgt.id) || emitted.has(tgt.id))
|
|
822
|
+
return done('goto', { gotoTarget: tgt.id });
|
|
823
|
+
if (LIFT_GATEWAYS.has(localName(tgt.$type))) {
|
|
824
|
+
// A decision only this path leads to: lift it here, as the arm's next statement.
|
|
825
|
+
taken.add(tgt.id);
|
|
826
|
+
const lift = planBranch(plan, tgt, emitted, false, taken);
|
|
827
|
+
if (!lift) {
|
|
828
|
+
taken.delete(tgt.id);
|
|
829
|
+
return done('goto', { gotoTarget: tgt.id });
|
|
830
|
+
}
|
|
831
|
+
for (const a of lift.arms)
|
|
832
|
+
for (const el of a.arm.elements)
|
|
833
|
+
taken.add(el.id);
|
|
834
|
+
if (lift.join)
|
|
835
|
+
taken.add(lift.join.id);
|
|
836
|
+
flows.push(...lift.consumedFlows);
|
|
837
|
+
labelKeptFlows(lift, labelKept);
|
|
838
|
+
items.push({ lift });
|
|
839
|
+
if (!lift.continuation)
|
|
840
|
+
return done('terminal');
|
|
841
|
+
// The construct's exit flow is already consumed; carry on AT the continuation.
|
|
842
|
+
f = undefined;
|
|
843
|
+
tgt = lift.continuation;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (!isArmNode(tgt))
|
|
847
|
+
return done('goto', { gotoTarget: tgt.id });
|
|
848
|
+
if (localName(tgt.$type) === 'endEvent') {
|
|
849
|
+
elements.push(tgt);
|
|
850
|
+
items.push({ el: tgt });
|
|
851
|
+
taken.add(tgt.id);
|
|
852
|
+
return done('terminal');
|
|
853
|
+
}
|
|
854
|
+
const outs = g.outs.get(tgt.id) ?? [];
|
|
855
|
+
// An element that fans out (or dead-ends) is not an arm's to own; the arm jumps to
|
|
856
|
+
// it and the top level writes it with its flows explicit.
|
|
857
|
+
if (outs.length !== 1)
|
|
858
|
+
return done('goto', { gotoTarget: tgt.id });
|
|
859
|
+
elements.push(tgt);
|
|
860
|
+
items.push({ el: tgt });
|
|
861
|
+
taken.add(tgt.id);
|
|
862
|
+
f = outs[0];
|
|
863
|
+
}
|
|
864
|
+
return fail('the path does not end');
|
|
865
|
+
}
|
|
866
|
+
/** Whether an event carries exactly one authorable definition, and which. */
|
|
867
|
+
function singleDefinition(el) {
|
|
868
|
+
const defs = el.eventDefinitions ?? [];
|
|
869
|
+
if (defs.length !== 1)
|
|
870
|
+
return undefined;
|
|
871
|
+
switch (defs[0].$type) {
|
|
872
|
+
case 'bpmn:ErrorEventDefinition':
|
|
873
|
+
return 'error';
|
|
874
|
+
case 'bpmn:TimerEventDefinition':
|
|
875
|
+
return 'timer';
|
|
876
|
+
case 'bpmn:MessageEventDefinition':
|
|
877
|
+
return 'message';
|
|
878
|
+
default:
|
|
879
|
+
return undefined;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Boundary events, lifted first: an activity's handlers are part of how the
|
|
884
|
+
* activity is rendered wherever it lands — at the top level or inside an arm.
|
|
885
|
+
*/
|
|
886
|
+
function planHandlers(plan) {
|
|
887
|
+
const { g } = plan;
|
|
888
|
+
for (const b of g.elements) {
|
|
889
|
+
if (localName(b.$type) !== 'boundaryEvent')
|
|
890
|
+
continue;
|
|
891
|
+
const host = g.byId.get(b.attachedToRef?.id);
|
|
892
|
+
const reject = (reason) => {
|
|
893
|
+
plan.summary.leftFlat.push({ id: b.id, reason });
|
|
894
|
+
};
|
|
895
|
+
if (!host || !isArmNode(host) || localName(host.$type) === 'endEvent') {
|
|
896
|
+
reject('attached to nothing an activity body can own');
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
if (!singleDefinition(b)) {
|
|
900
|
+
reject('carries no single error, timer or message definition');
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const outs = g.outs.get(b.id) ?? [];
|
|
904
|
+
if (outs.length !== 1) {
|
|
905
|
+
reject(`has ${outs.length} outgoing flows; a handler's path leaves once`);
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
const trial = new Set(plan.taken);
|
|
909
|
+
const arm = walkArm(plan, outs[0], trial, () => false, new Set());
|
|
910
|
+
if (!arm.ok) {
|
|
911
|
+
reject(arm.reason ?? 'path not liftable');
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const labelled = labelledImpliedFlow(arm.flows, false, arm.labelKept);
|
|
915
|
+
if (labelled) {
|
|
916
|
+
reject(`flow "${labelled.id}" on the handler's path carries the label "${labelled.name}", which an implied edge cannot`);
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
plan.taken.add(b.id);
|
|
920
|
+
for (const id of trial)
|
|
921
|
+
plan.taken.add(id);
|
|
922
|
+
for (const f of arm.flows)
|
|
923
|
+
implyFlow(plan, f);
|
|
924
|
+
plan.handlers.set(host.id, [...(plan.handlers.get(host.id) ?? []), { boundary: b, arm }]);
|
|
925
|
+
plan.summary.handlers++;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* A gateway and its arms as one construct, or nothing.
|
|
930
|
+
*
|
|
931
|
+
* @param mustTerminate - Inside an event sub-process body the construct is the last
|
|
932
|
+
* statement, so every arm has to end; a join would need a continuation.
|
|
933
|
+
*/
|
|
934
|
+
function planBranch(plan, gw, emitted, mustTerminate, seedTaken) {
|
|
935
|
+
const { g } = plan;
|
|
936
|
+
const kind = localName(gw.$type);
|
|
937
|
+
const reject = (reason) => {
|
|
938
|
+
plan.summary.leftFlat.push({ id: gw.id, reason });
|
|
939
|
+
return undefined;
|
|
940
|
+
};
|
|
941
|
+
const outs = g.outs.get(gw.id) ?? [];
|
|
942
|
+
if (outs.length < 2)
|
|
943
|
+
return reject('has fewer than two outgoing flows');
|
|
944
|
+
const construct = kind === 'parallelGateway' ? 'fork' : kind === 'eventBasedGateway' ? 'race' : 'choose';
|
|
945
|
+
const joinKind = construct === 'fork' ? 'parallelGateway' : construct === 'race' ? 'exclusiveGateway' : kind;
|
|
946
|
+
const defaultId = gw.default?.id;
|
|
947
|
+
if (construct === 'choose') {
|
|
948
|
+
for (const f of outs) {
|
|
949
|
+
const conditioned = f.conditionExpression?.body !== undefined;
|
|
950
|
+
if (f.id === defaultId && conditioned)
|
|
951
|
+
return reject(`default flow "${f.id}" also carries a condition`);
|
|
952
|
+
if (f.id !== defaultId && !conditioned)
|
|
953
|
+
return reject(`flow "${f.id}" has neither a condition nor the default`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
else if (outs.some((f) => f.conditionExpression?.body !== undefined)) {
|
|
957
|
+
return reject('a fork or race arm cannot carry a condition');
|
|
958
|
+
}
|
|
959
|
+
// An arm may own only what nothing has placed yet: an element already written at
|
|
960
|
+
// the top level (document order put it before its gateway) is reached with `.goto()`,
|
|
961
|
+
// not written a second time. Before flow ids could be relabelled almost no arm
|
|
962
|
+
// lifted on a real import, which is how this went unmeasured.
|
|
963
|
+
// A nested lift (this gateway met inside an arm) walks against the enclosing arm's
|
|
964
|
+
// ownership too, or its arms could claim what that arm already owns. The set is a
|
|
965
|
+
// trial copy: a rejected lift leaves nothing claimed.
|
|
966
|
+
const taken = new Set([...plan.taken, ...emitted, ...(seedTaken ?? [])]);
|
|
967
|
+
const arms = [];
|
|
968
|
+
const joinOk = (el) => LIFT_GATEWAYS.has(localName(el.$type)) && localName(el.$type) === joinKind && el.id !== gw.id;
|
|
969
|
+
for (const f of outs) {
|
|
970
|
+
const arm = walkArm(plan, f, taken, joinOk, emitted);
|
|
971
|
+
if (!arm.ok)
|
|
972
|
+
return reject(`arm from "${f.id}": ${arm.reason}`);
|
|
973
|
+
const labelled = labelledImpliedFlow(arm.flows, construct === 'choose', arm.labelKept);
|
|
974
|
+
if (labelled)
|
|
975
|
+
return reject(`flow "${labelled.id}" inside an arm carries the label "${labelled.name}", which an implied edge cannot`);
|
|
976
|
+
arms.push({ flow: f, arm, otherwise: f.id === defaultId });
|
|
977
|
+
}
|
|
978
|
+
// The join is the one gateway two or more arms reach; an arm that reaches a gateway
|
|
979
|
+
// alone, or a different one, jumps there instead — the builder writes a join only
|
|
980
|
+
// for two or more, and `.goto()` is exactly a flow to an element the arm does not own.
|
|
981
|
+
const reached = new Map();
|
|
982
|
+
for (const a of arms)
|
|
983
|
+
if (a.arm.end === 'join')
|
|
984
|
+
reached.set(a.arm.join.id, (reached.get(a.arm.join.id) ?? 0) + 1);
|
|
985
|
+
const shared = [...reached.entries()].filter(([, n]) => n >= 2).map(([id]) => id);
|
|
986
|
+
if (shared.length > 1)
|
|
987
|
+
return reject('arms rejoin at more than one shared gateway');
|
|
988
|
+
for (const a of arms) {
|
|
989
|
+
if (a.arm.end === 'join' && a.arm.join.id !== shared[0]) {
|
|
990
|
+
a.arm = { ...a.arm, end: 'goto', gotoTarget: a.arm.join.id, join: undefined };
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const joins = arms.filter((a) => a.arm.end === 'join');
|
|
994
|
+
const joinIds = new Set(joins.map((a) => a.arm.join.id));
|
|
995
|
+
if (arms.filter((a) => a.arm.end === 'join' && a.arm.elements.length === 0).length > 1)
|
|
996
|
+
return reject('more than one empty arm');
|
|
997
|
+
let join;
|
|
998
|
+
let continuation;
|
|
999
|
+
const consumedFlows = [];
|
|
1000
|
+
if (joinIds.size === 1) {
|
|
1001
|
+
if (mustTerminate)
|
|
1002
|
+
return reject('rejoins, where the enclosing body needs every arm to end');
|
|
1003
|
+
join = joins[0].arm.join;
|
|
1004
|
+
if ((g.ins.get(join.id) ?? []).length !== joins.length)
|
|
1005
|
+
return reject(`join "${join.id}" has incoming flows from outside the arms`);
|
|
1006
|
+
const jOuts = g.outs.get(join.id) ?? [];
|
|
1007
|
+
if (jOuts.length !== 1)
|
|
1008
|
+
return reject(`join "${join.id}" has ${jOuts.length} outgoing flows`);
|
|
1009
|
+
if (!plan.relabel && !derivableFlow(jOuts[0]))
|
|
1010
|
+
return reject(`flow "${jOuts[0].id}" out of the join is not named Flow_<source>_<target>`);
|
|
1011
|
+
if (labelledImpliedFlow([jOuts[0]], false))
|
|
1012
|
+
return reject(`flow "${jOuts[0].id}" out of the join carries a label, which an implied edge cannot`);
|
|
1013
|
+
continuation = g.byId.get(jOuts[0].targetRef?.id);
|
|
1014
|
+
if (!continuation)
|
|
1015
|
+
return reject('the join leaves the scope');
|
|
1016
|
+
if (emitted.has(continuation.id) || plan.taken.has(continuation.id)) {
|
|
1017
|
+
return reject(`continuation "${continuation.id}" was already placed, so the construct's exit could not reach it`);
|
|
1018
|
+
}
|
|
1019
|
+
consumedFlows.push(jOuts[0]);
|
|
1020
|
+
}
|
|
1021
|
+
for (const a of arms)
|
|
1022
|
+
consumedFlows.push(...a.arm.flows);
|
|
1023
|
+
return { gateway: gw, construct, inclusive: kind === 'inclusiveGateway', arms, join, continuation, consumedFlows };
|
|
1024
|
+
}
|
|
1025
|
+
/** Every element a lift owns — its arms' elements, nested constructs' included, and its joins. */
|
|
1026
|
+
function ownedByLift(lift) {
|
|
1027
|
+
const out = [lift.gateway.id];
|
|
1028
|
+
if (lift.join)
|
|
1029
|
+
out.push(lift.join.id);
|
|
1030
|
+
for (const a of lift.arms) {
|
|
1031
|
+
for (const el of a.arm.elements)
|
|
1032
|
+
out.push(el.id);
|
|
1033
|
+
for (const item of a.arm.items)
|
|
1034
|
+
if ('lift' in item)
|
|
1035
|
+
out.push(...ownedByLift(item.lift));
|
|
1036
|
+
}
|
|
1037
|
+
return out;
|
|
1038
|
+
}
|
|
1039
|
+
/** Every element a lift's arms jump to, nested constructs' included — the places the top level continues from. */
|
|
1040
|
+
function gotoTargetsOf(lift) {
|
|
1041
|
+
const out = [];
|
|
1042
|
+
for (const a of lift.arms) {
|
|
1043
|
+
if (a.arm.end === 'goto' && a.arm.gotoTarget)
|
|
1044
|
+
out.push(a.arm.gotoTarget);
|
|
1045
|
+
for (const item of a.arm.items)
|
|
1046
|
+
if ('lift' in item)
|
|
1047
|
+
out.push(...gotoTargetsOf(item.lift));
|
|
1048
|
+
}
|
|
1049
|
+
return out;
|
|
1050
|
+
}
|
|
1051
|
+
/** Render one element's call without a leading indent, its lifted handlers written into its body. */
|
|
1052
|
+
function renderElementNested(plan, el, depth, indent) {
|
|
1053
|
+
const tmp = new Chain('');
|
|
1054
|
+
emitElement(tmp, el, depth, indent);
|
|
1055
|
+
let call = tmp.all.join('\n');
|
|
1056
|
+
const handlers = plan.handlers.get(el.id);
|
|
1057
|
+
if (handlers?.length) {
|
|
1058
|
+
const pad = indent.repeat(depth + 2);
|
|
1059
|
+
const lines = handlers.map((h) => renderHandler(plan, h, depth + 2, indent));
|
|
1060
|
+
call = `${call.replace(/\)$/, '')}, (t) => {\n${pad}${lines.join(`\n${pad}`)}\n${indent.repeat(depth + 1)}})`;
|
|
1061
|
+
}
|
|
1062
|
+
return call;
|
|
1063
|
+
}
|
|
1064
|
+
/** The lines of an arm's body after `b`, one per element, or the closing `.goto()`. */
|
|
1065
|
+
function renderArmLines(plan, arm, depth, indent) {
|
|
1066
|
+
const lines = arm.items.map((item) => ('el' in item ? renderElementNested(plan, item.el, depth, indent) : renderBranch(plan, item.lift, depth, indent)));
|
|
1067
|
+
if (arm.end === 'goto')
|
|
1068
|
+
lines.push(`.goto(${str(arm.gotoTarget)})`);
|
|
1069
|
+
return lines;
|
|
1070
|
+
}
|
|
1071
|
+
function renderArmBody(plan, arm, depth, indent) {
|
|
1072
|
+
const lines = renderArmLines(plan, arm, depth, indent);
|
|
1073
|
+
if (!lines.length)
|
|
1074
|
+
return undefined;
|
|
1075
|
+
const pad = indent.repeat(depth + 1);
|
|
1076
|
+
return `(b) => b\n${pad}${lines.join(`\n${pad}`)}`;
|
|
1077
|
+
}
|
|
1078
|
+
function renderHandler(plan, h, depth, indent) {
|
|
1079
|
+
const b = h.boundary;
|
|
1080
|
+
const host = b.attachedToRef?.id;
|
|
1081
|
+
const def = singleDefinition(b);
|
|
1082
|
+
const method = def === 'error' ? 'onError' : def === 'timer' ? 'onTimer' : 'onMessage';
|
|
1083
|
+
const defExpr = eventOptions(b).find(([k]) => k === def)?.[1] ?? 'true';
|
|
1084
|
+
const payload = payloadOf(b);
|
|
1085
|
+
const type = extensionTypeOf(payload);
|
|
1086
|
+
const optsText = opts([
|
|
1087
|
+
['id', b.id !== `${host}_${method}` ? str(b.id) : undefined],
|
|
1088
|
+
['name', b.name !== undefined ? str(b.name) : undefined],
|
|
1089
|
+
['interrupting', def !== 'error' && b.cancelActivity === false ? 'false' : undefined],
|
|
1090
|
+
['payload', payload && type ? opts([['type', str(type)], ['outputRows', emitOutputRows(payload) ?? '[]']]) : undefined],
|
|
1091
|
+
]);
|
|
1092
|
+
const body = renderArmBody(plan, h.arm, depth, indent);
|
|
1093
|
+
const args = [defExpr, ...(optsText !== '' && optsText !== '{}' ? [optsText] : []), ...(body ? [body] : [])];
|
|
1094
|
+
return `t.${method}(${args.join(', ')});`;
|
|
1095
|
+
}
|
|
1096
|
+
function renderBranch(plan, lift, depth, indent) {
|
|
1097
|
+
plan.summary.branches++;
|
|
1098
|
+
// The chain already indents the statement by one level; arms sit one level inside
|
|
1099
|
+
// it, their bodies one further, and the closing bracket lines up with the statement.
|
|
1100
|
+
const pad = indent.repeat(depth + 2);
|
|
1101
|
+
const gw = lift.gateway;
|
|
1102
|
+
const joinOpt = lift.join !== undefined
|
|
1103
|
+
? opts([
|
|
1104
|
+
['id', lift.join.id !== `${gw.id}_join` ? str(lift.join.id) : undefined],
|
|
1105
|
+
['name', lift.join.name !== undefined ? str(lift.join.name) : undefined],
|
|
1106
|
+
])
|
|
1107
|
+
: undefined;
|
|
1108
|
+
const optsText = opts([
|
|
1109
|
+
['name', gw.name !== undefined ? str(gw.name) : undefined],
|
|
1110
|
+
['inclusive', lift.inclusive ? 'true' : undefined],
|
|
1111
|
+
['join', joinOpt !== undefined && joinOpt !== '' && joinOpt !== '{}' ? joinOpt : undefined],
|
|
1112
|
+
]);
|
|
1113
|
+
const armTexts = lift.arms.map(({ flow, arm, otherwise }) => {
|
|
1114
|
+
const body = renderArmBody(plan, arm, depth + 2, indent);
|
|
1115
|
+
if (lift.construct !== 'choose')
|
|
1116
|
+
return body ?? '() => {}';
|
|
1117
|
+
return opts([
|
|
1118
|
+
otherwise ? ['otherwise', 'true'] : ['when', str(flow.conditionExpression?.body)],
|
|
1119
|
+
['label', flow.name !== undefined ? str(flow.name) : undefined],
|
|
1120
|
+
['body', body],
|
|
1121
|
+
]);
|
|
1122
|
+
});
|
|
1123
|
+
const arms = `[\n${pad}${armTexts.join(`,\n${pad}`)},\n${indent.repeat(depth + 1)}]`;
|
|
1124
|
+
return `.${lift.construct}(${str(gw.id)}, ${arms}${optionalArg(optsText)})`;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* An event sub-process as `.eventSubProcess()`, or nothing.
|
|
1128
|
+
*
|
|
1129
|
+
* Its body is a `sequence` scope, so the whole body has to be one path from the
|
|
1130
|
+
* synthesized start — a chain, optionally ending in a construct whose arms all
|
|
1131
|
+
* end — with nothing left over.
|
|
1132
|
+
*/
|
|
1133
|
+
function renderEventSubProcess(plan, sp, depth, indent) {
|
|
1134
|
+
// A rejected body is written flat under its own flow ids, so the renames its
|
|
1135
|
+
// attempt recorded are withdrawn.
|
|
1136
|
+
const renamesBefore = plan.summary.renamedFlows.length;
|
|
1137
|
+
const reject = (reason) => {
|
|
1138
|
+
plan.summary.leftFlat.push({ id: sp.id, reason });
|
|
1139
|
+
plan.summary.renamedFlows.length = renamesBefore;
|
|
1140
|
+
return undefined;
|
|
1141
|
+
};
|
|
1142
|
+
if (retryOf(sp) || errorMappingOf(sp) || sp.loopCharacteristics)
|
|
1143
|
+
return reject('carries retry, error mapping or a loop');
|
|
1144
|
+
const g = scopeGraph(sp);
|
|
1145
|
+
const starts = g.elements.filter((el) => localName(el.$type) === 'startEvent');
|
|
1146
|
+
if (starts.length !== 1)
|
|
1147
|
+
return reject(`has ${starts.length} start events`);
|
|
1148
|
+
const start = starts[0];
|
|
1149
|
+
const def = singleDefinition(start);
|
|
1150
|
+
if (!def)
|
|
1151
|
+
return reject('its start carries no single error, timer or message definition');
|
|
1152
|
+
if (def === 'error' && start.isInterrupting === false)
|
|
1153
|
+
return reject('a non-interrupting error start is not a thing');
|
|
1154
|
+
const outs = g.outs.get(start.id) ?? [];
|
|
1155
|
+
if (outs.length !== 1)
|
|
1156
|
+
return reject(`its start has ${outs.length} outgoing flows`);
|
|
1157
|
+
// Shares the outer plan's pin list: a flow id is unique across the document, so every
|
|
1158
|
+
// pin can be declared once, in the one `.flowIds()` the top-level chain emits.
|
|
1159
|
+
const inner = {
|
|
1160
|
+
g,
|
|
1161
|
+
taken: new Set([start.id]),
|
|
1162
|
+
impliedFlows: new Set(),
|
|
1163
|
+
handlers: new Map(),
|
|
1164
|
+
summary: plan.summary,
|
|
1165
|
+
relabel: plan.relabel,
|
|
1166
|
+
pinned: plan.pinned,
|
|
1167
|
+
};
|
|
1168
|
+
planHandlers(inner);
|
|
1169
|
+
const arm = walkArm(inner, outs[0], inner.taken, (el) => LIFT_GATEWAYS.has(localName(el.$type)), new Set());
|
|
1170
|
+
if (!arm.ok)
|
|
1171
|
+
return reject(`body: ${arm.reason}`);
|
|
1172
|
+
const labelled = labelledImpliedFlow(arm.flows, false, arm.labelKept);
|
|
1173
|
+
if (labelled)
|
|
1174
|
+
return reject(`body: flow "${labelled.id}" carries the label "${labelled.name}", which an implied edge cannot`);
|
|
1175
|
+
for (const f of arm.flows)
|
|
1176
|
+
implyFlow(inner, f);
|
|
1177
|
+
const lines = renderArmLines(inner, arm, depth + 1, indent);
|
|
1178
|
+
if (arm.end === 'join') {
|
|
1179
|
+
// The chain reached a gateway: it may close the body as a construct whose arms all end.
|
|
1180
|
+
const gw = arm.join;
|
|
1181
|
+
if ((g.ins.get(gw.id) ?? []).length !== 1)
|
|
1182
|
+
return reject(`gateway "${gw.id}" in the body is reached from more than one place`);
|
|
1183
|
+
const lift = planBranch(inner, gw, new Set(), true);
|
|
1184
|
+
if (!lift)
|
|
1185
|
+
return reject(`gateway "${gw.id}" in the body could not be lifted`);
|
|
1186
|
+
for (const id of ownedByLift(lift))
|
|
1187
|
+
inner.taken.add(id);
|
|
1188
|
+
for (const f of lift.consumedFlows)
|
|
1189
|
+
implyFlow(inner, f);
|
|
1190
|
+
lines.push(renderBranch(inner, lift, depth + 1, indent));
|
|
1191
|
+
}
|
|
1192
|
+
else if (arm.end === 'goto') {
|
|
1193
|
+
return reject(`body jumps to "${arm.gotoTarget}", which a sequence body cannot`);
|
|
1194
|
+
}
|
|
1195
|
+
const leftover = g.elements.filter((el) => !inner.taken.has(el.id));
|
|
1196
|
+
if (leftover.length)
|
|
1197
|
+
return reject(`elements outside the one path: ${leftover.map((el) => `"${el.id}"`).join(', ')}`);
|
|
1198
|
+
if (g.flows.some((f) => !inner.impliedFlows.has(f.id)))
|
|
1199
|
+
return reject('flows outside the one path');
|
|
1200
|
+
// The start event's payload: the product's capture row is `errorVar`; anything else is spelled out.
|
|
1201
|
+
const payload = payloadOf(start);
|
|
1202
|
+
const type = extensionTypeOf(payload);
|
|
1203
|
+
const rows = payload?.output ?? [];
|
|
1204
|
+
const capture = def === 'error' && type === 'BPMN.Variables' && rows.length === 1 && rows[0].name === 'Error' && rows[0].source === '=Error' &&
|
|
1205
|
+
rows[0].type === 'jsonSchema' && rows[0].var !== undefined && (payload?.input ?? []).length === 0
|
|
1206
|
+
? rows[0].var
|
|
1207
|
+
: undefined;
|
|
1208
|
+
const defExpr = eventOptions(start).find(([k]) => k === def)?.[1];
|
|
1209
|
+
const spPayload = payloadOf(sp);
|
|
1210
|
+
const spType = extensionTypeOf(spPayload);
|
|
1211
|
+
const optsText = opts([
|
|
1212
|
+
['name', sp.name !== undefined ? str(sp.name) : undefined],
|
|
1213
|
+
[def, defExpr],
|
|
1214
|
+
['interrupting', def !== 'error' && start.isInterrupting === false ? 'false' : undefined],
|
|
1215
|
+
// `` `${id}_Error` `` is compile's default and `false` its opt-out, so neither is
|
|
1216
|
+
// left implicit the wrong way round: an error start with no capture row recompiles
|
|
1217
|
+
// without one, and a capture the designer named something else is spelled out.
|
|
1218
|
+
[
|
|
1219
|
+
'errorVar',
|
|
1220
|
+
capture === `${sp.id}_Error` ? undefined : capture !== undefined ? str(capture) : def === 'error' ? 'false' : undefined,
|
|
1221
|
+
],
|
|
1222
|
+
['startId', start.id !== `${sp.id}_start` ? str(start.id) : undefined],
|
|
1223
|
+
['startName', start.name !== undefined ? str(start.name) : undefined],
|
|
1224
|
+
['startPayload', payload && type && capture === undefined ? opts([['type', str(type)], ['outputRows', emitOutputRows(payload) ?? '[]']]) : undefined],
|
|
1225
|
+
['tags', tagsOf(sp)],
|
|
1226
|
+
['payload', spPayload && spType ? opts([['type', str(spType)], ['outputRows', emitOutputRows(spPayload) ?? '[]']]) : undefined],
|
|
1227
|
+
]);
|
|
1228
|
+
const vars = new Chain('');
|
|
1229
|
+
emitVariablesInto(vars, sp, sp.id, capture !== undefined && captureDeclLooksDerived(sp, capture, start.id) ? new Set([capture]) : undefined);
|
|
1230
|
+
const bodyLines = [...vars.all, ...lines];
|
|
1231
|
+
const pad = indent.repeat(depth + 2);
|
|
1232
|
+
const body = bodyLines.length ? `(h) => h\n${pad}${bodyLines.join(`\n${pad}`)}` : '(h) => h';
|
|
1233
|
+
plan.summary.nets++;
|
|
1234
|
+
return `.eventSubProcess(${str(sp.id)}, ${optsText}, ${body})`;
|
|
1235
|
+
}
|
|
1236
|
+
/** `emitScope`, with the liftable regions written as constructs. */
|
|
1237
|
+
function emitScopeNested(chain, container, depth, indent, summary, relabel, sequence) {
|
|
1238
|
+
const g = scopeGraph(container);
|
|
1239
|
+
const plan = { g, taken: new Set(), impliedFlows: new Set(), handlers: new Map(), summary, relabel, pinned: [] };
|
|
1240
|
+
planHandlers(plan);
|
|
1241
|
+
const emitted = new Set();
|
|
1242
|
+
const written = new Set();
|
|
1243
|
+
if (sequence)
|
|
1244
|
+
chain.add(".flowMode('sequence')");
|
|
1245
|
+
const renderFlow = (f) => `.sequenceFlow(${str(f.sourceRef?.id)}, ${str(f.targetRef?.id)}${optionalArg(opts([
|
|
1246
|
+
['id', f.id !== derivedFlowId(f) ? str(f.id) : undefined],
|
|
1247
|
+
['name', f.name !== undefined ? str(f.name) : undefined],
|
|
1248
|
+
['condition', f.conditionExpression?.body !== undefined ? str(f.conditionExpression.body) : undefined],
|
|
1249
|
+
]))})`;
|
|
1250
|
+
/** Write an element's remaining flows now, before the next statement — so "explicit wins" applies. */
|
|
1251
|
+
const writeNow = (flows) => {
|
|
1252
|
+
for (const f of flows) {
|
|
1253
|
+
if (plan.impliedFlows.has(f.id) || written.has(f.id))
|
|
1254
|
+
continue;
|
|
1255
|
+
written.add(f.id);
|
|
1256
|
+
chain.add(renderFlow(f));
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
/**
|
|
1260
|
+
* Write one element and say where the path continues: a lifted construct's exit, or
|
|
1261
|
+
* the one element a single outgoing flow leads to. `undefined` ends the chain.
|
|
1262
|
+
*/
|
|
1263
|
+
const emitOne = (el) => {
|
|
1264
|
+
emitted.add(el.id);
|
|
1265
|
+
if (LIFT_GATEWAYS.has(localName(el.$type))) {
|
|
1266
|
+
const lift = planBranch(plan, el, emitted, false);
|
|
1267
|
+
if (lift) {
|
|
1268
|
+
for (const id of ownedByLift(lift))
|
|
1269
|
+
plan.taken.add(id);
|
|
1270
|
+
for (const f of lift.consumedFlows)
|
|
1271
|
+
implyFlow(plan, f);
|
|
1272
|
+
chain.add(renderBranch(plan, lift, depth, indent));
|
|
1273
|
+
pending.push(...gotoTargetsOf(lift));
|
|
1274
|
+
return lift.continuation;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
if (isEventSubProcess(el)) {
|
|
1278
|
+
const call = renderEventSubProcess(plan, el, depth, indent);
|
|
1279
|
+
if (call) {
|
|
1280
|
+
chain.add(call);
|
|
1281
|
+
return undefined;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
chain.add(renderElementNested(plan, el, depth, indent));
|
|
1285
|
+
for (const h of plan.handlers.get(el.id) ?? [])
|
|
1286
|
+
if (h.arm.end === 'goto' && h.arm.gotoTarget)
|
|
1287
|
+
pending.push(h.arm.gotoTarget);
|
|
1288
|
+
const outs = g.outs.get(el.id) ?? [];
|
|
1289
|
+
if (outs.length === 1) {
|
|
1290
|
+
const f = outs[0];
|
|
1291
|
+
const next = g.byId.get(f.targetRef?.id);
|
|
1292
|
+
const fresh = next !== undefined && !emitted.has(next.id) && !plan.taken.has(next.id);
|
|
1293
|
+
// In sequence mode the edge to the statement written next is implied — when that
|
|
1294
|
+
// statement IS the successor, the edge may be relabelled (or already carries the
|
|
1295
|
+
// derived id), and it carries no label. Otherwise it is written here, before the
|
|
1296
|
+
// next statement, so the builder does not imply a second one beside it.
|
|
1297
|
+
if (sequence && fresh && (relabel || derivableFlow(f)) && !labelledImpliedFlow([f], false))
|
|
1298
|
+
implyFlow(plan, f);
|
|
1299
|
+
else if (sequence)
|
|
1300
|
+
writeNow([f]);
|
|
1301
|
+
return fresh ? next : undefined;
|
|
1302
|
+
}
|
|
1303
|
+
// A fan-out: its edges are explicit, written now, and each target is a place the
|
|
1304
|
+
// process continues from.
|
|
1305
|
+
if (sequence)
|
|
1306
|
+
writeNow(outs);
|
|
1307
|
+
for (const f of outs)
|
|
1308
|
+
pending.push(f.targetRef?.id);
|
|
1309
|
+
return undefined;
|
|
1310
|
+
};
|
|
1311
|
+
// Emit in CONTROL-FLOW order, not document order: from each source, follow the
|
|
1312
|
+
// path while it is a path. An import lists elements in the order they were drawn,
|
|
1313
|
+
// which put a gateway after the elements its arms should own and had the arms jump
|
|
1314
|
+
// to them instead; walking from the start writes the gateway first, the arm owns
|
|
1315
|
+
// its chain, and what remains explicit is a merge or a back edge — the same
|
|
1316
|
+
// ordering an author would type. Manual starts first, then other starts, then
|
|
1317
|
+
// anything nothing flows into, then whatever a cycle left unreached.
|
|
1318
|
+
// The worklist: where the process continues after a construct's arms jumped out or
|
|
1319
|
+
// an element fanned out. Drained before the next source, so the top level follows
|
|
1320
|
+
// the graph's reach rather than the document's order.
|
|
1321
|
+
const pending = [];
|
|
1322
|
+
const emitFrom = (start) => {
|
|
1323
|
+
let el = start;
|
|
1324
|
+
while (el && !emitted.has(el.id) && !plan.taken.has(el.id))
|
|
1325
|
+
el = emitOne(el);
|
|
1326
|
+
while (pending.length) {
|
|
1327
|
+
const next = g.byId.get(pending.shift() ?? '');
|
|
1328
|
+
if (next && !emitted.has(next.id) && !plan.taken.has(next.id))
|
|
1329
|
+
emitFrom(next);
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
const rank = (el) => {
|
|
1333
|
+
const kind = localName(el.$type);
|
|
1334
|
+
if (kind === 'startEvent')
|
|
1335
|
+
return (el.eventDefinitions ?? []).length === 0 && payloadOf(el)?.$type !== 'uipath:Event' ? 0 : 1;
|
|
1336
|
+
return (g.ins.get(el.id) ?? []).length === 0 ? 2 : 3;
|
|
1337
|
+
};
|
|
1338
|
+
const sources = [...g.elements].sort((a, b) => rank(a) - rank(b) || g.elements.indexOf(a) - g.elements.indexOf(b));
|
|
1339
|
+
for (const el of sources)
|
|
1340
|
+
emitFrom(el);
|
|
1341
|
+
for (const f of g.flows) {
|
|
1342
|
+
if (plan.impliedFlows.has(f.id) || written.has(f.id))
|
|
1343
|
+
continue;
|
|
1344
|
+
const auto = `Flow_${f.sourceRef?.id}_${f.targetRef?.id}`;
|
|
1345
|
+
chain.add(`.sequenceFlow(${str(f.sourceRef?.id)}, ${str(f.targetRef?.id)}${optionalArg(opts([
|
|
1346
|
+
['id', f.id !== auto ? str(f.id) : undefined],
|
|
1347
|
+
['name', f.name !== undefined ? str(f.name) : undefined],
|
|
1348
|
+
['condition', f.conditionExpression?.body !== undefined ? str(f.conditionExpression.body) : undefined],
|
|
1349
|
+
]))})`);
|
|
1350
|
+
}
|
|
1351
|
+
// Last, so it is first: the implied edges whose id is not the one the construct
|
|
1352
|
+
// derives. Emitted at the front of the chain because it is a declaration about the
|
|
1353
|
+
// whole scope, and only knowable once the walk has decided what to imply.
|
|
1354
|
+
if (plan.pinned.length) {
|
|
1355
|
+
const rows = plan.pinned.map((e) => `{ source: ${str(e.source)}, target: ${str(e.target)}, id: ${str(e.id)} }`);
|
|
1356
|
+
chain.addFirst(`.flowIds([${rows.join(', ')}])`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
597
1359
|
/**
|
|
598
1360
|
* Whether a start event's event definition carries configuration a registry spec
|
|
599
1361
|
* cannot supply — today, a timer's duration/date/cycle.
|
|
@@ -715,6 +1477,9 @@ function emitEvent(chain, el, kind, payload, type) {
|
|
|
715
1477
|
chain.add(`.${kind}(${str(el.id)}${optionalArg(opts([
|
|
716
1478
|
['name', el.name !== undefined ? str(el.name) : undefined],
|
|
717
1479
|
...eventOptions(el),
|
|
1480
|
+
// Only a start event can be non-interrupting, and only the exception is
|
|
1481
|
+
// spelled — the builder's default is BPMN's.
|
|
1482
|
+
['interrupting', kind === 'startEvent' && el.isInterrupting === false ? 'false' : undefined],
|
|
718
1483
|
['payload', eventPayload],
|
|
719
1484
|
]))})`);
|
|
720
1485
|
}
|
|
@@ -812,8 +1577,10 @@ function emitElement(chain, el, depth, indent) {
|
|
|
812
1577
|
return;
|
|
813
1578
|
case 'subProcess': {
|
|
814
1579
|
const inner = new Chain(indent.repeat(depth + 2));
|
|
1580
|
+
if (pinExplicitInBodies)
|
|
1581
|
+
inner.add(".flowMode('explicit')");
|
|
815
1582
|
emitScope(inner, el, depth + 1, indent);
|
|
816
|
-
emitVariablesInto(inner, el);
|
|
1583
|
+
emitVariablesInto(inner, el, el.id);
|
|
817
1584
|
const body = inner.all.length
|
|
818
1585
|
? `(sp) => {\n${inner.all.map((l, i) => (i === 0 ? `${l.replace(/^(\s*)\./, '$1sp.')}` : l)).join('\n')};\n${indent.repeat(depth + 1)}}`
|
|
819
1586
|
: '(sp) => {}';
|
|
@@ -859,15 +1626,47 @@ function varType(wire) {
|
|
|
859
1626
|
return wire ?? 'string';
|
|
860
1627
|
}
|
|
861
1628
|
}
|
|
862
|
-
/**
|
|
863
|
-
function
|
|
1629
|
+
/** How a declaration's `elementId` is spelled in source — see `emitVariablesInto`. */
|
|
1630
|
+
function emitElementId(elementId, ownerId, rehome) {
|
|
1631
|
+
if (elementId === undefined)
|
|
1632
|
+
return 'null';
|
|
1633
|
+
const effective = rehome && elementId === rehome.from ? rehome.to : elementId;
|
|
1634
|
+
return effective === ownerId ? undefined : str(effective);
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Whether an error net's capture variable is declared exactly as `errorVar` would
|
|
1638
|
+
* declare it — `jsonSchema`, named `Error`, scoped to the start event, no default — so
|
|
1639
|
+
* the nested form can leave it to the default instead of echoing it.
|
|
1640
|
+
*/
|
|
1641
|
+
function captureDeclLooksDerived(sp, varId, startId) {
|
|
1642
|
+
const vars = (sp.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:Variables');
|
|
1643
|
+
const v = (vars?.inputOutput ?? []).find((x) => x.id === varId);
|
|
1644
|
+
return v !== undefined && v.name === 'Error' && v.type === 'jsonSchema' && v.elementId === startId && v.default === undefined;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Emit `.var()` / `.input()` / `.output()` for one scope's declarations.
|
|
1648
|
+
*
|
|
1649
|
+
* `elementId` is written the way `compile` reads it: a declaration belonging to the
|
|
1650
|
+
* scope itself (`ownerId` — the process or sub-process id, which is the compile-time
|
|
1651
|
+
* default) is emitted with no `elementId`; one belonging to another element names it;
|
|
1652
|
+
* and one with NO `elementId` on the wire is emitted as `elementId: null`, because the
|
|
1653
|
+
* default would otherwise add one on recompile and the round trip would not be byte
|
|
1654
|
+
* for byte. Older exports have that shape on every variable.
|
|
1655
|
+
*/
|
|
1656
|
+
function emitVariablesInto(chain, container, ownerId, skip = new Set(),
|
|
864
1657
|
/**
|
|
865
1658
|
* Rewrite one `elementId` as another. Needed when the declarations are read off a
|
|
866
1659
|
* COLLABORATION, whose id will not exist after a recompile — see `variablesHost`.
|
|
867
1660
|
*/
|
|
868
1661
|
rehome,
|
|
869
1662
|
/** How each schema body should be written — see {@link planSchemas}. */
|
|
870
|
-
plan
|
|
1663
|
+
plan,
|
|
1664
|
+
/**
|
|
1665
|
+
* The scope the serializer would give an unscoped declaration of this kind — see
|
|
1666
|
+
* `publicDeclarationScope` in serialize.ts. A declaration already there is emitted
|
|
1667
|
+
* without `elementId`; the owner when absent.
|
|
1668
|
+
*/
|
|
1669
|
+
defaultScope = () => ownerId) {
|
|
871
1670
|
const vars = (container.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:Variables');
|
|
872
1671
|
if (!vars)
|
|
873
1672
|
return;
|
|
@@ -887,12 +1686,7 @@ plan) {
|
|
|
887
1686
|
['default', def],
|
|
888
1687
|
['schema', v.body !== undefined ? (plan?.ref.get(String(v.body)) ?? jsonLiteral(v.body)) : undefined],
|
|
889
1688
|
['required', v.required !== undefined ? 'true' : undefined],
|
|
890
|
-
[
|
|
891
|
-
'elementId',
|
|
892
|
-
v.elementId === undefined
|
|
893
|
-
? undefined
|
|
894
|
-
: str(rehome && v.elementId === rehome.from ? rehome.to : v.elementId),
|
|
895
|
-
],
|
|
1689
|
+
['elementId', emitElementId(v.elementId, defaultScope(prop, v), rehome)],
|
|
896
1690
|
]))})`);
|
|
897
1691
|
}
|
|
898
1692
|
}
|
|
@@ -1034,8 +1828,22 @@ export async function decompile(xml, options = {}) {
|
|
|
1034
1828
|
]);
|
|
1035
1829
|
for (const [id, body] of schemaPlan.declare)
|
|
1036
1830
|
chain.add(`.schema(${str(id)}, ${body})`);
|
|
1037
|
-
emitVariablesInto(chain, host.container, derived, host.rehome, schemaPlan);
|
|
1038
|
-
|
|
1831
|
+
emitVariablesInto(chain, host.container, host.rehome?.to ?? host.container.id, derived, host.rehome, schemaPlan, publicDeclarationScopeOf(process, host.rehome?.to ?? host.container.id));
|
|
1832
|
+
if (options.style === 'nested') {
|
|
1833
|
+
const sequence = options.sequenceMode ?? true;
|
|
1834
|
+
const summary = { handlers: 0, branches: 0, nets: 0, leftFlat: [], renamedFlows: [], sequence };
|
|
1835
|
+
pinExplicitInBodies = sequence;
|
|
1836
|
+
try {
|
|
1837
|
+
emitScopeNested(chain, process, 0, indent, summary, options.relabelFlows ?? true, sequence);
|
|
1838
|
+
}
|
|
1839
|
+
finally {
|
|
1840
|
+
pinExplicitInBodies = false;
|
|
1841
|
+
}
|
|
1842
|
+
options.onLift?.(summary);
|
|
1843
|
+
}
|
|
1844
|
+
else {
|
|
1845
|
+
emitScope(chain, process, 0, indent);
|
|
1846
|
+
}
|
|
1039
1847
|
const importFrom = options.importSpecifier ?? './bpmn-sdk.js';
|
|
1040
1848
|
const imported = ['bpmn', ...[...schemaPlan.imports].sort()].join(', ');
|
|
1041
1849
|
const head = `import { ${imported} } from ${str(importFrom)};\n\nexport default bpmn(${str(process.id)})`;
|
|
@@ -1069,17 +1877,21 @@ export async function decompile(xml, options = {}) {
|
|
|
1069
1877
|
* `decision` while its declared variable is `o`.
|
|
1070
1878
|
*/
|
|
1071
1879
|
function derivedVariableIds(process) {
|
|
1072
|
-
|
|
1880
|
+
// var id -> [owner element id, wire type the derived declaration carries]
|
|
1881
|
+
const candidates = new Map();
|
|
1073
1882
|
const walk = (container) => {
|
|
1074
1883
|
for (const el of container.flowElements ?? []) {
|
|
1075
1884
|
const payload = payloadOf(el);
|
|
1076
1885
|
const type = extensionTypeOf(payload);
|
|
1077
|
-
// A
|
|
1078
|
-
//
|
|
1079
|
-
|
|
1886
|
+
// A variable task's rows write AUTHORED variables (its assignments), so they are
|
|
1887
|
+
// not derived. A script task's `outputs` rows ARE — serialize declares each as a
|
|
1888
|
+
// `string` owned by the task, because the validator does not read the row as a
|
|
1889
|
+
// declaration — so they are candidates with the type that shape carries.
|
|
1890
|
+
if (payload && type && type !== 'BPMN.Variables') {
|
|
1891
|
+
const wire = type === 'BPMN.ScriptTask' ? 'string' : 'json';
|
|
1080
1892
|
for (const row of payload.output ?? [])
|
|
1081
1893
|
if (row.var)
|
|
1082
|
-
candidates.
|
|
1894
|
+
candidates.set(row.var, [el.id, wire]);
|
|
1083
1895
|
}
|
|
1084
1896
|
if (el.$type === 'bpmn:SubProcess')
|
|
1085
1897
|
walk(el);
|
|
@@ -1087,20 +1899,22 @@ function derivedVariableIds(process) {
|
|
|
1087
1899
|
};
|
|
1088
1900
|
walk(process);
|
|
1089
1901
|
// Keep only the candidates whose declaration matches what `implicitNodeVars` would
|
|
1090
|
-
// write: `
|
|
1091
|
-
// `
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1902
|
+
// write: an `inputOutput` row, `name` equal to `id`, the wire type above, no default,
|
|
1903
|
+
// and `elementId` naming the node whose row fills it. A declaration that differs in
|
|
1904
|
+
// ANY of those cannot have been derived, so it is the author's and has to be echoed
|
|
1905
|
+
// — including the older shape with no `elementId`, which recompiles identically only
|
|
1906
|
+
// because `emitVariablesInto` then writes it as `elementId: null`.
|
|
1094
1907
|
const out = new Set();
|
|
1095
1908
|
const vars = (process.extensionElements?.values ?? []).find((x) => x.$type === 'uipath:Variables');
|
|
1096
1909
|
for (const v of vars?.inputOutput ?? []) {
|
|
1097
|
-
|
|
1910
|
+
const c = candidates.get(v.id);
|
|
1911
|
+
if (!c)
|
|
1098
1912
|
continue;
|
|
1099
1913
|
if (v.name !== undefined && v.name !== v.id)
|
|
1100
1914
|
continue;
|
|
1101
|
-
if (v.type !==
|
|
1915
|
+
if (v.type !== c[1])
|
|
1102
1916
|
continue;
|
|
1103
|
-
if (v.default !== undefined || v.elementId !==
|
|
1917
|
+
if (v.default !== undefined || v.elementId !== c[0])
|
|
1104
1918
|
continue;
|
|
1105
1919
|
out.add(v.id);
|
|
1106
1920
|
}
|