@uipath/maestro-builder-sdk 6.25.1 → 6.27.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/check.js CHANGED
@@ -29,7 +29,7 @@ import { ownEntry } from './core/own-entry.js';
29
29
  import { FLOW_NODE_TYPES } from './flow-node-registry.js';
30
30
  import { stepExits } from './flow-step-exits.js';
31
31
  import { plainHttpOnErrorMessage } from './nodes/http/flow/family.js';
32
- import { resolveVersion } from './core/nodes/versioned.js';
32
+ import { resolveVersion, resolveVersionCarried } from './core/nodes/versioned.js';
33
33
  import { declaredErrorPortRefusal } from './core/error-port.js';
34
34
  export function check(built, opts = {}) {
35
35
  let diags = [];
@@ -275,7 +275,7 @@ function checkFlow(built, ancestors, opts = {}) {
275
275
  declared.add(s.name); // a step's output is referenceable by name
276
276
  }
277
277
  if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port === 'error')
278
- checkOnError(s, prevPortSource, diags, prevPortSource && checkedSteps.get(prevPortSource.name));
278
+ checkOnError(s, prevPortSource, diags, prevPortSource && checkedSteps.get(prevPortSource.name), carriedLookup([built, ...ancestors]));
279
279
  if ((s.kind === 'stepToList' || s.kind === 'stepToRef') && s.port.startsWith('branch-')) {
280
280
  const want = s.port.slice('branch-'.length);
281
281
  const prevSpec = prevAction?.spec;
@@ -381,7 +381,7 @@ function checkFlow(built, ancestors, opts = {}) {
381
381
  prevPortSource = s;
382
382
  const registered = registeredKind(s.spec);
383
383
  if (registered) {
384
- const { placed, checked } = checkRegisteredStep(registered, s, exprs, errorSource, opts, diags, typeOf, triggerId);
384
+ const { placed, checked } = checkRegisteredStep(registered, s, exprs, errorSource, opts, diags, typeOf, triggerId, carriedLookup([built, ...ancestors]));
385
385
  checkedSteps.set(s.name, checked);
386
386
  if (placed)
387
387
  registeredSteps.set(s.name, checked);
@@ -518,13 +518,51 @@ function checkFlow(built, ancestors, opts = {}) {
518
518
  walk(s.otherwise, inLoop);
519
519
  }
520
520
  else if (s.kind === 'switch') {
521
- addExpr(s.on.js, s.name);
521
+ if (s.on)
522
+ addExpr(s.on.js, s.name);
523
+ for (const c of s.cases)
524
+ if (c.when)
525
+ addExpr(c.when.js, s.name);
522
526
  switchNames.add(s.name);
527
+ s.cases.forEach((c, i) => {
528
+ const hasValue = c.value !== undefined;
529
+ const hasWhen = c.when !== undefined;
530
+ if (hasValue === hasWhen || (hasValue && !s.on)) {
531
+ diags.push({
532
+ level: 'error',
533
+ code: 'SWITCH_CASE_FORM',
534
+ message: hasValue && hasWhen
535
+ ? `Switch "${s.name}" case ${i + 1} has both \`value\` and \`when\`. Give it one: \`value\` is compared with \`on\`, \`when\` is the case's own condition.`
536
+ : hasValue
537
+ ? `Switch "${s.name}" case ${i + 1} has a \`value\` but the switch has no \`on\` to compare it with. Pass \`on\`, or give the case a \`when\` condition.`
538
+ : `Switch "${s.name}" case ${i + 1} has neither \`value\` nor \`when\`.`,
539
+ step: s.name,
540
+ });
541
+ }
542
+ if (hasWhen && c.when.literal) {
543
+ diags.push({
544
+ level: 'error',
545
+ code: 'SWITCH_CONST_WHEN',
546
+ message: `Switch "${s.name}" case ${i + 1}'s \`when\` is the constant ${JSON.stringify(c.when.value)}, so it is always or never taken. Pass an expression.`,
547
+ step: s.name,
548
+ });
549
+ }
550
+ });
523
551
  // A constant discriminant can never match a case — it is what
524
552
  // `.switch('q', 'quarter', …)` (a name where an expression belongs)
525
553
  // degrades into once types are stripped, and it would emit
526
554
  // `"quarter" === 2`: valid JS, always false, default arm every time.
527
- if (s.on.literal)
555
+ // `on` is compared only with `value` cases; with none, it is unused.
556
+ const hasValueCase = s.cases.some((c) => c.value !== undefined);
557
+ if (s.on && !hasValueCase && s.cases.length > 0)
558
+ diags.push({
559
+ level: 'warning',
560
+ code: 'SWITCH_ON_UNUSED',
561
+ message: `Switch "${s.name}" has an \`on\`, but every case has its own \`when\` condition, so \`on\` is never compared. `
562
+ + 'Pass `undefined` for `on`.',
563
+ step: s.name,
564
+ });
565
+ if (s.on?.literal && hasValueCase)
528
566
  diags.push({
529
567
  level: 'error',
530
568
  code: 'SWITCH_CONST_ON',
@@ -563,12 +601,12 @@ function checkFlow(built, ancestors, opts = {}) {
563
601
  seenIds.add(String(caseId));
564
602
  });
565
603
  for (const c of s.cases) {
566
- const key = `${typeof c.value}:${String(c.value)}`;
604
+ const key = c.when !== undefined ? `when:${c.when.js}` : `${typeof c.value}:${String(c.value)}`;
567
605
  if (seenValues.has(key))
568
606
  diags.push({
569
607
  level: 'error',
570
608
  code: 'DUP_CASE',
571
- message: `Switch "${s.name}" has two cases for ${JSON.stringify(c.value)}; the second is unreachable `
609
+ message: `Switch "${s.name}" has two cases for ${c.when !== undefined ? `when ${c.when.js}` : JSON.stringify(c.value)}; the second is unreachable `
572
610
  + '(the first matching case wins).',
573
611
  step: s.name,
574
612
  });
@@ -577,7 +615,7 @@ function checkFlow(built, ancestors, opts = {}) {
577
615
  diags.push({
578
616
  level: 'warning',
579
617
  code: 'EMPTY_ARM',
580
- message: `Switch "${s.name}" has an empty arm for case ${JSON.stringify(c.value)}.`,
618
+ message: `Switch "${s.name}" has an empty arm for case ${c.when !== undefined ? `when ${c.when.js}` : JSON.stringify(c.value)}.`,
581
619
  step: s.name,
582
620
  });
583
621
  walk(c.body, inLoop);
@@ -3935,6 +3973,8 @@ function reportUnreachableHitlOutcomes(choices, exprs, diags) {
3935
3973
  * and so are object/array/any/file discriminants — nothing useful to compare.
3936
3974
  */
3937
3975
  function checkCaseValueTypes(s, typeOf, diags, triggerId) {
3976
+ if (!s.on)
3977
+ return;
3938
3978
  const refName = declaredRefName(s.on.js, triggerId);
3939
3979
  if (!refName)
3940
3980
  return;
@@ -3946,6 +3986,8 @@ function checkCaseValueTypes(s, typeOf, diags, triggerId) {
3946
3986
  if (!expected)
3947
3987
  return;
3948
3988
  for (const c of s.cases) {
3989
+ if (c.value === undefined)
3990
+ continue;
3949
3991
  const actual = typeof c.value;
3950
3992
  if (actual === expected)
3951
3993
  continue;
@@ -3977,6 +4019,11 @@ const LOOP_FIELDS = new Set(['currentItem', 'currentIteration', 'collection', 'o
3977
4019
  * before the 2.4 default (#868); its own fields are not audited here.
3978
4020
  */
3979
4021
  const LOOP_FIELDS_1_0_0 = new Set(['currentItem', 'currentIndex', 'collection', 'output', 'error']);
4022
+ /** The {@link CarriedLookup} over `scopes` — the flow being checked first, then its ancestors. */
4023
+ function carriedLookup(scopes) {
4024
+ const all = scopes.flatMap((b) => b.definitions ?? []);
4025
+ return Object.assign((typeName, version) => all.find((d) => d.nodeType === typeName && d.version === version), { versions: (typeName) => [...new Set(all.filter((d) => d.nodeType === typeName).map((d) => String(d.version)))] });
4026
+ }
3980
4027
  /** The registered node type for a spec's kind, or undefined (a custom FlowAction has no kind). */
3981
4028
  function registeredKind(spec) {
3982
4029
  const kind = spec?.kind;
@@ -3994,14 +4041,15 @@ function registeredKind(spec) {
3994
4041
  * which serialize enforces (#942) and the type's tests pin; an author cannot
3995
4042
  * cause the gap, so there is no author diagnostic to report.
3996
4043
  */
3997
- function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf, triggerId) {
4044
+ function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf, triggerId, carried) {
3998
4045
  const spec = s.spec;
3999
4046
  /** The step's node-local rules and expressions, as `impl` defines them. */
4000
- const runRules = (impl) => {
4047
+ const runRules = (impl, version = impl.version, carriedDef) => {
4001
4048
  const ctx = {
4002
4049
  step: s.name,
4003
- version: impl.version,
4004
- definition: () => bundledDefinition(impl.typeName, impl.version),
4050
+ // At a carried version, the version and definition serialize emits.
4051
+ version,
4052
+ definition: () => carriedDef ?? bundledDefinition(impl.typeName, impl.version),
4005
4053
  report: (code, message, suggestion) => diags.push(catalogued(entry, s.name, code, message, suggestion)),
4006
4054
  declaredRef: (js) => {
4007
4055
  const name = declaredRefName(js, triggerId);
@@ -4024,8 +4072,14 @@ function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf,
4024
4072
  return ctx;
4025
4073
  };
4026
4074
  let impl;
4075
+ let version;
4076
+ let carriedDef;
4027
4077
  try {
4028
- impl = resolveVersion(entry.type, spec, s.options?.version, `step "${s.name}"`);
4078
+ // A pin the registry lacks but the flow carries resolves (#1128), as in serialize.
4079
+ const resolved = resolveVersionCarried(entry.type, spec, s.options?.version, `step "${s.name}"`, carried);
4080
+ impl = resolved.impl;
4081
+ version = resolved.version;
4082
+ carriedDef = resolved.carried;
4029
4083
  }
4030
4084
  catch (e) {
4031
4085
  // A select() that picks an unregistered version is the node type's bug, not the author's pin.
@@ -4043,8 +4097,14 @@ function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf,
4043
4097
  checked: { entry, impl: fallback, spec, placement: { step: s.name, kind: fallback.kind, spec, version: fallback.version }, ctx: fallbackCtx },
4044
4098
  };
4045
4099
  }
4046
- const ctx = runRules(impl);
4047
- return { placed: true, checked: { entry, impl, spec, placement: { step: s.name, kind: impl.kind, spec, version: impl.version }, ctx } };
4100
+ const ctx = runRules(impl, version, carriedDef);
4101
+ return {
4102
+ placed: true,
4103
+ checked: {
4104
+ entry, impl, spec, placement: { step: s.name, kind: impl.kind, spec, version }, ctx,
4105
+ ...(carriedDef !== undefined ? { carried: carriedDef } : {}),
4106
+ },
4107
+ };
4048
4108
  }
4049
4109
  /** Every non-literal Expr reachable in `value`, as expressions to check. */
4050
4110
  function pushDeepExprs(value, step, errorSource, out) {
@@ -4171,7 +4231,7 @@ const ERROR_ENVELOPE_FIELDS = ['code', 'message', 'detail', 'category', 'status'
4171
4231
  */
4172
4232
  function checkOnError(_handler, step, diags,
4173
4233
  /** The step's registry record, when its kind is registered and its version resolved. */
4174
- placed) {
4234
+ placed, carried) {
4175
4235
  // The builder refuses a handler with no eligible step before it, so a missing
4176
4236
  // source here means a hand-built `BuiltFlow`; there is nothing to say about it.
4177
4237
  if (!step)
@@ -4243,7 +4303,7 @@ placed) {
4243
4303
  }
4244
4304
  }
4245
4305
  else {
4246
- const def = bundledDefinition(impl.typeName, impl.version);
4306
+ const def = placed.carried ?? bundledDefinition(impl.typeName, impl.version);
4247
4307
  if (def !== undefined && !definitionHasErrorPort(def)) {
4248
4308
  diags.push({ level: 'error', code: 'ONERROR_NO_ERROR_PORT', message: typedNoErrorPortMessage(step.name, impl.kind, def), step: step.name });
4249
4309
  return;
@@ -4262,7 +4322,12 @@ placed) {
4262
4322
  // A version the SDK does not bundle (an inline agent's registry version) has
4263
4323
  // no definition here to ask; version resolution reports that, not this check.
4264
4324
  const bundledType = bundledActionNodeType(spec);
4265
- const bundledDef = bundledType ? bundledDefinition(bundledType, actionVersion(spec, step.options?.version)) : undefined;
4325
+ // A version the flow CARRIES (`.definitions()`, #1128) answers the same way
4326
+ // a bundled one does: it is the definition serialize will emit.
4327
+ const pinned = step.options?.version;
4328
+ const bundledDef = bundledType
4329
+ ? bundledDefinition(bundledType, actionVersion(spec, pinned)) ?? (pinned !== undefined ? carried?.(bundledType, pinned) : undefined)
4330
+ : undefined;
4266
4331
  // A per-tenant family: ask the definition serialize synthesizes for it (#1040).
4267
4332
  let perCallDef;
4268
4333
  if (!bundledType) {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Node definitions a flow CARRIES — the third source a pinned `{ version }`
3
+ * resolves from, after the SDK's bundle (#1128).
4
+ *
5
+ * Every `.flow` file holds the exact definition of every node it uses, in
6
+ * `definitions[]`. The SDK bundles only the versions it was released with, so
7
+ * a version the registry served later (`node add` writes
8
+ * `uipath.human-in-the-loop.quick-form@1.6`; the SDK ships 1.0 and 1.1) used
9
+ * to be refused, and the source decompile wrote for an untouched product flow
10
+ * did not compile. A carried definition closes that: decompile copies the
11
+ * file's own entry into `.definitions([...])`, and compile resolves the pin
12
+ * against it.
13
+ *
14
+ * A carried definition is an exact copy of a real one, never a way to invent a
15
+ * version. So it is validated the way a `rawNode` snapshot manifest is: it
16
+ * names its own `nodeType` and `version`, and it has the `display.label` every
17
+ * node manifest has.
18
+ */
19
+ /** A carried definition, by its `<nodeType>@<version>` key. */
20
+ export type CarriedDefinitions = ReadonlyMap<string, Record<string, unknown>>;
21
+ /**
22
+ * Why `manifest` cannot be carried, or `undefined` when it can.
23
+ *
24
+ * For instance `{ nodeType: 'x', version: '1.0' }` is refused for lacking
25
+ * `display.label`.
26
+ */
27
+ export declare function carriedDefinitionRefusal(manifest: unknown, where: string): string | undefined;
28
+ /**
29
+ * Index `manifests` by `<nodeType>@<version>`. Throws on an invalid manifest,
30
+ * or on two different manifests for one pair (which one compiles would be a
31
+ * guess).
32
+ */
33
+ export declare function indexCarriedDefinitions(manifests: readonly unknown[], where: string, into?: Map<string, Record<string, unknown>>): Map<string, Record<string, unknown>>;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Node definitions a flow CARRIES — the third source a pinned `{ version }`
3
+ * resolves from, after the SDK's bundle (#1128).
4
+ *
5
+ * Every `.flow` file holds the exact definition of every node it uses, in
6
+ * `definitions[]`. The SDK bundles only the versions it was released with, so
7
+ * a version the registry served later (`node add` writes
8
+ * `uipath.human-in-the-loop.quick-form@1.6`; the SDK ships 1.0 and 1.1) used
9
+ * to be refused, and the source decompile wrote for an untouched product flow
10
+ * did not compile. A carried definition closes that: decompile copies the
11
+ * file's own entry into `.definitions([...])`, and compile resolves the pin
12
+ * against it.
13
+ *
14
+ * A carried definition is an exact copy of a real one, never a way to invent a
15
+ * version. So it is validated the way a `rawNode` snapshot manifest is: it
16
+ * names its own `nodeType` and `version`, and it has the `display.label` every
17
+ * node manifest has.
18
+ */
19
+ /**
20
+ * Why `manifest` cannot be carried, or `undefined` when it can.
21
+ *
22
+ * For instance `{ nodeType: 'x', version: '1.0' }` is refused for lacking
23
+ * `display.label`.
24
+ */
25
+ export function carriedDefinitionRefusal(manifest, where) {
26
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
27
+ return `${where}: a carried definition must be a node manifest object.`;
28
+ }
29
+ const m = manifest;
30
+ if (typeof m.nodeType !== 'string' || !m.nodeType || typeof m.version !== 'string' || !m.version) {
31
+ return `${where}: a carried definition must name its exact nodeType and version.`;
32
+ }
33
+ if (typeof m.display?.label !== 'string' || !m.display.label) {
34
+ return `${where}: the carried definition ${m.nodeType}@${m.version} lacks display.label — not a valid node manifest.`;
35
+ }
36
+ return undefined;
37
+ }
38
+ /**
39
+ * Index `manifests` by `<nodeType>@<version>`. Throws on an invalid manifest,
40
+ * or on two different manifests for one pair (which one compiles would be a
41
+ * guess).
42
+ */
43
+ export function indexCarriedDefinitions(manifests, where, into = new Map()) {
44
+ for (const m of manifests) {
45
+ const refusal = carriedDefinitionRefusal(m, where);
46
+ if (refusal)
47
+ throw new Error(refusal);
48
+ const def = m;
49
+ const key = `${def.nodeType}@${def.version}`;
50
+ const prior = into.get(key);
51
+ if (prior !== undefined && stableJson(prior) !== stableJson(def)) {
52
+ throw new Error(`${where}: two different carried definitions for ${key}. Keep the one the file holds.`);
53
+ }
54
+ into.set(key, def);
55
+ }
56
+ return into;
57
+ }
58
+ /** `JSON.stringify` with object keys sorted, so two copies of one manifest compare equal whatever their key order. */
59
+ function stableJson(value) {
60
+ return JSON.stringify(value, (_key, v) => (v && typeof v === 'object' && !Array.isArray(v)
61
+ ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]]))
62
+ : v));
63
+ }
@@ -48,3 +48,38 @@ export declare function asVersioned<Ctx extends ArtifactContexts>(t: AnyNodeType
48
48
  * node type's bug, not the author's, and throws as an internal SDK error.
49
49
  */
50
50
  export declare function resolveVersion<Spec, Ctx extends ArtifactContexts>(t: VersionedNodeType<Spec, unknown, Ctx>, spec: Spec, requested: string | undefined, where: string): NodeType<Spec, unknown, Ctx>;
51
+ /**
52
+ * Looks up a definition the flow carries (`.definitions()`) for `typeName@version`.
53
+ * `versions`, when present, lists every version of `typeName` the flow carries,
54
+ * so a refusal can name them.
55
+ */
56
+ export type CarriedDefinitionLookup = ((typeName: string, version: string) => Record<string, unknown> | undefined) & {
57
+ readonly versions?: (typeName: string) => readonly string[];
58
+ };
59
+ /** What {@link resolveVersionCarried} resolved: the implementation, the version to emit, and a carried definition when one answered. */
60
+ export interface ResolvedNodeVersion<Impl> {
61
+ readonly impl: Impl;
62
+ /** The version the node is emitted at — `impl.version`, or the carried version. */
63
+ readonly version: string;
64
+ /** The definition the FLOW carries for `typeName@version`, when the registry has no implementation at that version. */
65
+ readonly carried?: Record<string, unknown>;
66
+ }
67
+ /**
68
+ * {@link resolveVersion} with the third definition source (#1128): a definition
69
+ * the flow CARRIES (`.definitions()`, decompile's copy of the file's own
70
+ * `definitions[]` entry).
71
+ *
72
+ * Order: a registered version resolves as before. An explicit version the type
73
+ * does not register, but the flow carries, is emitted AT that version with the
74
+ * carried definition, through the implementation the type would use unpinned
75
+ * (`select(spec)`, else the default) — the registry has no implementation of
76
+ * its own for it, and the node keeps the version the file pins instead of being
77
+ * refused. Anything else is refused as {@link resolveVersion} refuses it, except
78
+ * that an unregistered pin also names the versions the flow carries and the
79
+ * `.definitions()` remedy.
80
+ *
81
+ * For instance a file pinning `core.action.script@1.0` (registered: 1.0.0, 1.1)
82
+ * and carrying that definition emits the node at 1.0 through the default
83
+ * implementation.
84
+ */
85
+ export declare function resolveVersionCarried<Spec, Ctx extends ArtifactContexts>(t: VersionedNodeType<Spec, unknown, Ctx>, spec: Spec, requested: string | undefined, where: string, carried: CarriedDefinitionLookup | undefined): ResolvedNodeVersion<NodeType<Spec, unknown, Ctx>>;
@@ -44,3 +44,38 @@ export function resolveVersion(t, spec, requested, where) {
44
44
  }
45
45
  return impl;
46
46
  }
47
+ /**
48
+ * {@link resolveVersion} with the third definition source (#1128): a definition
49
+ * the flow CARRIES (`.definitions()`, decompile's copy of the file's own
50
+ * `definitions[]` entry).
51
+ *
52
+ * Order: a registered version resolves as before. An explicit version the type
53
+ * does not register, but the flow carries, is emitted AT that version with the
54
+ * carried definition, through the implementation the type would use unpinned
55
+ * (`select(spec)`, else the default) — the registry has no implementation of
56
+ * its own for it, and the node keeps the version the file pins instead of being
57
+ * refused. Anything else is refused as {@link resolveVersion} refuses it, except
58
+ * that an unregistered pin also names the versions the flow carries and the
59
+ * `.definitions()` remedy.
60
+ *
61
+ * For instance a file pinning `core.action.script@1.0` (registered: 1.0.0, 1.1)
62
+ * and carrying that definition emits the node at 1.0 through the default
63
+ * implementation.
64
+ */
65
+ export function resolveVersionCarried(t, spec, requested, where, carried) {
66
+ if (requested !== undefined && !ownEntry(t.versions, requested)) {
67
+ const def = carried?.(t.typeName, requested);
68
+ if (def !== undefined) {
69
+ return { impl: resolveVersion(t, spec, undefined, where), version: requested, carried: def };
70
+ }
71
+ // Refused: name both sources, and the way to add the second one.
72
+ const registered = Object.keys(t.versions).sort().join(', ');
73
+ const carriedVersions = [...(carried?.versions?.(t.typeName) ?? [])].sort();
74
+ throw new Error(`${where} requests ${t.typeName}@${requested}, but this SDK registers only ${registered}`
75
+ + (carriedVersions.length > 0 ? ` and the flow carries only ${carriedVersions.join(', ')}` : '')
76
+ + '. Versions are exact — omit { version } to use the default, request one listed here, or carry '
77
+ + "that version's definition (the .flow file's definitions[] entry) with .definitions([...]).");
78
+ }
79
+ const impl = resolveVersion(t, spec, requested, where);
80
+ return { impl, version: impl.version };
81
+ }