@uipath/maestro-builder-sdk 6.51.3 → 6.52.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.
@@ -6,19 +6,57 @@ function owns(p, typeName) {
6
6
  }
7
7
  export class NodeTypeRegistry {
8
8
  artifact;
9
- families;
10
- #byKind = new Map();
11
- #byTypeName = new Map();
12
- /** The types that own a family of names (`typeNamePrefix`); no two prefixes overlap. */
13
- #byPrefix = [];
9
+ #builtIn;
10
+ #extensions = [];
11
+ #index;
12
+ #options;
14
13
  /**
15
14
  * @param artifact - The artifact's name, for messages (`'flow'`).
16
- * @param families - Every family this artifact supports, in no significant order.
15
+ * @param families - Every built-in family this artifact supports, in no significant order.
17
16
  * @param options - What the artifact knows that the contract does not.
18
17
  */
19
18
  constructor(artifact, families, options) {
20
19
  this.artifact = artifact;
21
- this.families = families;
20
+ this.#builtIn = families;
21
+ this.#options = options;
22
+ const { index, problems } = this.#build(families);
23
+ if (problems.length > 0) {
24
+ throw new Error(`internal SDK error: the ${artifact} node-type registry is inconsistent:\n`
25
+ + problems.map((p) => ` - ${p}`).join('\n'));
26
+ }
27
+ this.#index = index;
28
+ }
29
+ /** Every family the registry knows: the built-in ones, then the registered packages' in registration order. */
30
+ get families() {
31
+ return [...this.#builtIn, ...this.#extensions];
32
+ }
33
+ /**
34
+ * Register a node-type package's family (`@uipath/maestro-builder-sdk/extend`).
35
+ *
36
+ * The whole registry is re-indexed with the new family last, under the same
37
+ * rules construction applies, so a package cannot take a kind, a type name, a
38
+ * prefix or a diagnostic code a built-in family owns, nor one another package
39
+ * registered first. A refusal names the package and leaves the live index as
40
+ * it was. Registering the same family object twice is a no-op: an ESM module
41
+ * runs once per process, but a test may import a package through two paths.
42
+ */
43
+ register(family) {
44
+ if (this.#extensions.includes(family))
45
+ return;
46
+ if (family.package === undefined) {
47
+ throw new Error(`cannot register node family "${family.id}": a family registered at run time must name its package (NodeFamily.package).`);
48
+ }
49
+ const { index, problems } = this.#build([...this.#builtIn, ...this.#extensions, family]);
50
+ if (problems.length > 0) {
51
+ throw new Error(`cannot register node family "${family.id}" from ${family.package.name}:\n`
52
+ + problems.map((p) => ` - ${p}`).join('\n'));
53
+ }
54
+ this.#extensions = [...this.#extensions, family];
55
+ this.#index = index;
56
+ }
57
+ #build(families) {
58
+ const index = { byKind: new Map(), byTypeName: new Map(), byPrefix: [] };
59
+ const options = this.#options;
22
60
  const problems = [];
23
61
  const familyIds = new Set();
24
62
  const codeOwner = new Map();
@@ -36,7 +74,7 @@ export class NodeTypeRegistry {
36
74
  for (const listed of family.types) {
37
75
  const type = asVersioned(listed);
38
76
  const where = `${family.id}/${type.kind}`;
39
- const sharing = this.#byKind.get(type.kind);
77
+ const sharing = index.byKind.get(type.kind);
40
78
  if (sharing && (!type.matches || sharing.some((other) => !other.type.matches))) {
41
79
  problems.push(`kind "${type.kind}" is registered twice (${[...sharing, { family, type }].map((e) => `${e.family.id}/${e.type.typeName}`).join(' and ')}); node types that share a kind must each declare matches(spec)`);
42
80
  }
@@ -45,22 +83,22 @@ export class NodeTypeRegistry {
45
83
  if (type.typeName !== `${prefix}*`) {
46
84
  problems.push(`${where}: a type that owns the names under "${prefix}" is named "${prefix}*", not "${type.typeName}"`);
47
85
  }
48
- const overlap = this.#byPrefix.find((p) => p.prefix.startsWith(prefix) || prefix.startsWith(p.prefix));
86
+ const overlap = index.byPrefix.find((p) => p.prefix.startsWith(prefix) || prefix.startsWith(p.prefix));
49
87
  if (overlap)
50
88
  problems.push(`${where}: prefix "${prefix}" overlaps "${overlap.prefix}" (${placeOf(overlap.entry)})`);
51
- const shadowed = [...this.#byTypeName.keys()].find((n) => n.startsWith(prefix) && (type.typeNameMatches?.(n) ?? true));
89
+ const shadowed = [...index.byTypeName.keys()].find((n) => n.startsWith(prefix) && (type.typeNameMatches?.(n) ?? true));
52
90
  if (shadowed)
53
91
  problems.push(`${where}: prefix "${prefix}" would also own the registered type "${shadowed}"`);
54
92
  }
55
93
  else {
56
- const owner = this.#byPrefix.find((p) => owns(p, type.typeName));
94
+ const owner = index.byPrefix.find((p) => owns(p, type.typeName));
57
95
  if (owner)
58
96
  problems.push(`${where}: type name "${type.typeName}" falls under the prefix "${owner.prefix}" (${placeOf(owner.entry)})`);
59
97
  }
60
98
  if (type.typeNameMatches !== undefined && type.typeNamePrefix === undefined) {
61
99
  problems.push(`${where}: typeNameMatches narrows a typeNamePrefix, and the type declares none`);
62
100
  }
63
- const typeOwner = this.#byTypeName.get(type.typeName);
101
+ const typeOwner = index.byTypeName.get(type.typeName);
64
102
  if (typeOwner) {
65
103
  problems.push(`type name "${type.typeName}" is registered twice (${placeOf(typeOwner)} and ${where}); one VersionedNodeType owns every version of a type`);
66
104
  }
@@ -72,19 +110,24 @@ export class NodeTypeRegistry {
72
110
  }
73
111
  for (const [key, impl] of Object.entries(type.versions)) {
74
112
  problems.push(...implementationProblems(where, key, impl, type, options));
113
+ // A package's definition is the package's: the platform does not run it, so
114
+ // a built-in family may not declare one, and a package family declares nothing else.
115
+ if (impl.definition === 'package' && family.package === undefined) {
116
+ problems.push(`${where}@${key}: is a package definition, so it must come from a node-type package (NodeFamily.package), not a built-in family`);
117
+ }
118
+ if (impl.definition !== 'package' && family.package !== undefined) {
119
+ problems.push(`${where}@${key}: comes from package ${family.package.name}, so it must declare definition: 'package' (found '${impl.definition}')`);
120
+ }
75
121
  }
76
122
  const entry = { family, type };
77
- this.#byKind.set(type.kind, [...(sharing ?? []), entry]);
78
- this.#byTypeName.set(type.typeName, entry);
123
+ index.byKind.set(type.kind, [...(sharing ?? []), entry]);
124
+ index.byTypeName.set(type.typeName, entry);
79
125
  if (type.typeNamePrefix !== undefined) {
80
- this.#byPrefix.push({ prefix: type.typeNamePrefix, matches: type.typeNameMatches?.bind(type), entry });
126
+ index.byPrefix.push({ prefix: type.typeNamePrefix, matches: type.typeNameMatches?.bind(type), entry });
81
127
  }
82
128
  }
83
129
  }
84
- if (problems.length > 0) {
85
- throw new Error(`internal SDK error: the ${artifact} node-type registry is inconsistent:\n`
86
- + problems.map((p) => ` - ${p}`).join('\n'));
87
- }
130
+ return { index, problems };
88
131
  }
89
132
  /**
90
133
  * The node type for a spec, by its kind, or undefined when the kind is not
@@ -92,7 +135,7 @@ export class NodeTypeRegistry {
92
135
  * holds — undefined when none does; two matching is an SDK bug and throws.
93
136
  */
94
137
  byKind(kind, spec) {
95
- const all = this.#byKind.get(kind);
138
+ const all = this.#index.byKind.get(kind);
96
139
  if (!all)
97
140
  return undefined;
98
141
  if (all.length === 1)
@@ -105,7 +148,7 @@ export class NodeTypeRegistry {
105
148
  }
106
149
  /** Every node type registered for a kind (several when they share it), in registration order. */
107
150
  typesOfKind(kind) {
108
- return this.#byKind.get(kind) ?? [];
151
+ return this.#index.byKind.get(kind) ?? [];
109
152
  }
110
153
  /**
111
154
  * The node type for a type name in the file — the exact name, else the type
@@ -115,14 +158,14 @@ export class NodeTypeRegistry {
115
158
  * read it as its factory, so it still does.
116
159
  */
117
160
  byTypeName(typeName) {
118
- const exact = this.#byTypeName.get(typeName);
161
+ const exact = this.#index.byTypeName.get(typeName);
119
162
  if (exact)
120
163
  return exact;
121
- return this.#byPrefix.find((p) => owns(p, typeName))?.entry;
164
+ return this.#index.byPrefix.find((p) => owns(p, typeName))?.entry;
122
165
  }
123
166
  /** Every registered kind, sorted. */
124
167
  kinds() {
125
- return [...this.#byKind.keys()].sort();
168
+ return [...this.#index.byKind.keys()].sort();
126
169
  }
127
170
  }
128
171
  /** Where a registered type lives, as the registry's messages name it: `<family>/<kind>`. */
@@ -154,7 +197,7 @@ function implementationProblems(where, key, impl, type, options) {
154
197
  }
155
198
  }
156
199
  else if (impl.errorPort === undefined) {
157
- out.push(`${at}: is per-tenant, so it must declare errorPort — there is no definition to ask`);
200
+ out.push(`${at}: is ${impl.definition}, so it must declare errorPort — there is no bundled definition to ask`);
158
201
  }
159
202
  return out;
160
203
  }
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  import { type CliExitCode } from './cli-run.js';
3
- export declare function run(argv: string[]): CliExitCode;
3
+ export declare function run(argv: string[]): Promise<CliExitCode>;
@@ -23,9 +23,10 @@ import { basename, dirname, join, resolve } from 'node:path';
23
23
  import { defaultSourceOut, workDirBeside, writeSource } from './workdir.js';
24
24
  import { decompile } from './decompile.js';
25
25
  import { resolveFlowRefs } from './ref-resolve.js';
26
- import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
26
+ import { flagValue, flagValues, runWhenInvokedDirectly } from './cli-run.js';
27
27
  import { PACKAGE_NAME } from './package-name.js';
28
28
  import { commentText } from './core/comment-text.js';
29
+ import { loadNodePackages } from './node-packages.js';
29
30
  import { pipelineFrontDoor } from './registry/uip/pipeline-front-door.js';
30
31
  /** Derive a PascalCase base name from the flow name/id for the default output file. */
31
32
  function baseName(flow) {
@@ -37,12 +38,12 @@ function baseName(flow) {
37
38
  .join('');
38
39
  return pascal || 'Flow';
39
40
  }
40
- export function run(argv) {
41
+ export async function run(argv) {
41
42
  if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
42
- console.error('usage: flow-decompile <original.flow> [-o Name.flow.ts] [--import <specifier>] [--strict]');
43
+ console.error('usage: flow-decompile <original.flow> [-o Name.flow.ts] [--import <specifier>] [--package <name>]... [--strict]');
43
44
  return 2;
44
45
  }
45
- const flags = new Set(['-o', '--import']);
46
+ const flags = new Set(['-o', '--import', '--package']);
46
47
  const pos = argv.find((a, i) => !flags.has(a) && !flags.has(argv[i - 1]) && !a.startsWith('--'));
47
48
  if (!pos) {
48
49
  console.error('flow-decompile: missing <original.flow>');
@@ -55,6 +56,15 @@ export function run(argv) {
55
56
  baseUri: pos,
56
57
  read: (path) => readFileSync(path, 'utf8'),
57
58
  });
59
+ // Node-type packages the file needs: loaded only when the project trusts them
60
+ // (package.json nodePackages, or --package), resolved from the .flow's own
61
+ // directory (node-packages.ts). An untrusted or missing package is a warning,
62
+ // and its nodes print as rawNode(...).
63
+ const packages = await loadNodePackages(pos, flow, flagValues(argv, '--package'));
64
+ for (const message of packages.warnings)
65
+ console.error(`flow-decompile: warning: ${message}`);
66
+ if (packages.loaded.length > 0)
67
+ reportResult({ packages: packages.loaded.map((p) => p.package) });
58
68
  const ts = decompile(flow, {
59
69
  importSpecifier: flagValue(argv, '--import') ?? PACKAGE_NAME,
60
70
  bestEffort: !argv.includes('--strict'),
@@ -152,5 +152,19 @@ export interface DecompileOptions {
152
152
  */
153
153
  readSidecar?: (relativePath: string) => string | undefined;
154
154
  }
155
+ /**
156
+ * Tracks which identifiers the emitted source uses, per module, so each import
157
+ * is exact. A built-in factory lands in the SDK's import; a node-type package's
158
+ * factory (`FactoryCall.module`, the family's `package.name`) lands in that
159
+ * package's own import line, so the emitted source compiles against the same
160
+ * packages the original was written with (docs/EXTENSIBLE_NODE_TYPES_DESIGN.md §3.5).
161
+ */
162
+ /**
163
+ * The SDK names the decompiler itself may import (its helpers), beside every
164
+ * built-in family's factory. A package factory with one of these names is
165
+ * aliased, whichever is requested first, so the SDK's own name is always bare.
166
+ * `decompile-imports.test.ts` checks this list against the `imp.use('…')` sites.
167
+ */
168
+ export declare const SDK_HELPER_NAMES: readonly string[];
155
169
  export declare function decompile(flow: FlowFile, options?: DecompileOptions): string;
156
170
  export {};
package/dist/decompile.js CHANGED
@@ -308,16 +308,76 @@ class Emit {
308
308
  return this.lines.join('\n');
309
309
  }
310
310
  }
311
- /** Tracks which SDK identifiers the emitted source uses, so the import is exact. */
311
+ /**
312
+ * Tracks which identifiers the emitted source uses, per module, so each import
313
+ * is exact. A built-in factory lands in the SDK's import; a node-type package's
314
+ * factory (`FactoryCall.module`, the family's `package.name`) lands in that
315
+ * package's own import line, so the emitted source compiles against the same
316
+ * packages the original was written with (docs/EXTENSIBLE_NODE_TYPES_DESIGN.md §3.5).
317
+ */
318
+ /**
319
+ * The SDK names the decompiler itself may import (its helpers), beside every
320
+ * built-in family's factory. A package factory with one of these names is
321
+ * aliased, whichever is requested first, so the SDK's own name is always bare.
322
+ * `decompile-imports.test.ts` checks this list against the `imp.use('…')` sites.
323
+ */
324
+ export const SDK_HELPER_NAMES = [
325
+ 'connector', 'conversationTrigger', 'err', 'flow', 'formTrigger', 'input', 'ixpExtract', 'js', 'lit', 'manual',
326
+ 'mock', 'onEvent', 'out', 'ran', 'rawNode', 'scheduled', 'subflow', 'types', 'v', 'voiceTrigger', 'waitForEvent',
327
+ ];
328
+ /** `@acme/flow-nodes` → `acme_flow_nodes`: the alias suffix for a package's colliding factory. */
329
+ function moduleSlug(module) {
330
+ return module.replace(/^@/, '').replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
331
+ }
312
332
  class Imports {
313
- used = new Set(['flow']);
314
- use(name) {
315
- this.used.add(name);
316
- return name;
333
+ /** `undefined` is the SDK itself; the specifier is only known when the lines are printed. */
334
+ used = new Map([[undefined, new Map([['flow', 'flow']])]]);
335
+ /** Every local identifier handed out, so no two modules bind the same name. */
336
+ locals = new Set(['flow']);
337
+ /** Names a package may not take bare: the SDK's helpers and every built-in factory. */
338
+ reserved;
339
+ constructor() {
340
+ this.reserved = new Set([
341
+ ...SDK_HELPER_NAMES,
342
+ ...FLOW_NODE_TYPES.families.filter((f) => f.package === undefined).flatMap((f) => f.types.map((t) => t.factory)),
343
+ ]);
344
+ }
345
+ /**
346
+ * The local identifier to print for `name` imported from `module` (`undefined`
347
+ * for the SDK). An SDK name is always its own; a package factory keeps its name
348
+ * unless the SDK owns it or another module already binds it, and is then
349
+ * imported `as <name>_<package slug>` — deterministic, so the emitted source is
350
+ * stable and `import { post as post_acme_flow_nodes }` reads as what it is.
351
+ */
352
+ use(name, module) {
353
+ let names = this.used.get(module);
354
+ if (names === undefined) {
355
+ names = new Map();
356
+ this.used.set(module, names);
357
+ }
358
+ const existing = names.get(name);
359
+ if (existing !== undefined)
360
+ return existing;
361
+ let local = name;
362
+ if (module !== undefined && (this.reserved.has(name) || this.locals.has(name))) {
363
+ local = `${name}_${moduleSlug(module)}`;
364
+ for (let n = 2; this.locals.has(local); n++)
365
+ local = `${name}_${moduleSlug(module)}_${n}`;
366
+ }
367
+ names.set(name, local);
368
+ this.locals.add(local);
369
+ return local;
317
370
  }
371
+ /** One import per module: the SDK first, then the packages by name. */
318
372
  line(specifier) {
319
- const names = [...this.used].sort();
320
- return `import { ${names.join(', ')} } from ${tsLiteral(specifier)};`;
373
+ const lines = [];
374
+ for (const [module, names] of [...this.used].sort(([a], [b]) => (a === undefined ? -1 : b === undefined ? 1 : a.localeCompare(b)))) {
375
+ if (names.size === 0)
376
+ continue;
377
+ const bindings = [...names].sort(([a], [b]) => a.localeCompare(b)).map(([name, local]) => (local === name ? name : `${name} as ${local}`));
378
+ lines.push(`import { ${bindings.join(', ')} } from ${tsLiteral(module ?? specifier)};`);
379
+ }
380
+ return lines.join('\n');
321
381
  }
322
382
  }
323
383
  // ─── Expression inversion ─────────────────────────────────────────────────────
@@ -774,7 +834,9 @@ function emitRegisteredSpec(node, imp, inputNames, graph) {
774
834
  };
775
835
  const options = opts((call.options ?? []).map(([k, v]) => [k, v]));
776
836
  const args = [...(call.positional ?? []), ...(options ? [options] : [])];
777
- return `${imp.use(call.factory)}(${args.join(', ')})`
837
+ // A package type's factory is imported from its package, not from the SDK.
838
+ const origin = FLOW_NODE_TYPES.byTypeName(node.type)?.family.package;
839
+ return `${imp.use(call.factory, call.module ?? origin?.name)}(${args.join(', ')})`
778
840
  + (call.comment !== undefined ? ` /* ${commentText(call.comment)} */` : '');
779
841
  }
780
842
  /**
@@ -937,12 +999,18 @@ function unsupportedSpec(node, imp, o) {
937
999
  }
938
1000
  const inputs = node.inputs ?? {};
939
1001
  const inputsSrc = Object.keys(inputs).length > 0 ? JSON.stringify(inputs, null, 2) : undefined;
1002
+ // A definition a node-type package wrote names the package; the node prints
1003
+ // as rawNode because the project does not trust (or has not installed) it.
1004
+ const origin = def.source?.package;
1005
+ const hint = typeof origin === 'string'
1006
+ ? ` /* from node-type package ${commentText(origin)}: load it with --package or package.json nodePackages to get its factory */`
1007
+ : '';
940
1008
  return `${imp.use('rawNode')}(${opts([
941
1009
  ['nodeType', tsLiteral(node.type)],
942
1010
  ['version', tsText(node.typeVersion)],
943
1011
  ['manifest', constName],
944
1012
  ['inputs', inputsSrc],
945
- ])})`;
1013
+ ])})${hint}`;
946
1014
  }
947
1015
  if (o.bestEffort === false) {
948
1016
  throw new Error(`decompile: unsupported node type "${node.type}" (node "${node.id}") and the file carries no `
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The extension surface of the Flow SDK: what a NODE-TYPE PACKAGE imports.
3
+ *
4
+ * A node-type package carries a family of node types the SDK does not ship —
5
+ * the Local family (`@uipath/flow-local-nodes`) is the first — and makes them
6
+ * available to a `.flow.ts` through a normal import:
7
+ *
8
+ * ```ts
9
+ * import { flow, manual } from '@uipath/maestro-builder-sdk';
10
+ * import { runCommand } from '@uipath/flow-local-nodes';
11
+ * ```
12
+ *
13
+ * The package registers its family as a side effect of being imported
14
+ * (`defineFlowNodeFamily`), so `compile`, `check` and `merge` need no
15
+ * configuration: `loadBuiltFlow` imports the author's module, the module imports
16
+ * the package, the package registers, and `build()` runs against a registry that
17
+ * knows the new kinds. Every type of a package family declares
18
+ * `definition: 'package'`, and the registry refuses a package type anywhere else —
19
+ * that is the rule that keeps the public SDK free of node types the UiPath
20
+ * platform cannot run.
21
+ *
22
+ * Everything else in `core/` is private. A package needs only what is exported
23
+ * here; see docs/EXTENSIBLE_NODE_TYPES_DESIGN.md.
24
+ */
25
+ import type { NodeFamily, DiagnosticCatalog, PackageOrigin } from './core/nodes/family.js';
26
+ import type { FlowAction } from './core/node-classes.js';
27
+ import type { FlowContexts, FlowNodeFamily } from './flow-node-type.js';
28
+ export type { NodeType, FactoryCall, OutputContract, ExprSite, DefinitionSource, ArtifactContexts, FieldsRead, OutputRead, OutputPathRead, } from './core/nodes/node-type.js';
29
+ export type { VersionedNodeType } from './core/nodes/versioned.js';
30
+ export type { NodeFamily, Placement, DiagnosticCatalog, PackageOrigin } from './core/nodes/family.js';
31
+ export type { DefinitionReference } from './core/node-classes.js';
32
+ export type { FlowContexts, FlowNodeType, FlowVersionedNodeType, FlowNodeFamily, FlowSerializeContext, FlowDeserializeContext, FlowPrintContext, FlowCheckContext, FlowFamilyCheckContext, FlowNodeContribution, FlowRawNodeAdvice, } from './flow-node-type.js';
33
+ export type { FlowAction } from './core/node-classes.js';
34
+ export { defineNodeDefinition, DEFAULT_PACKAGE_DEFINITION_VERSION, } from './core/nodes/package-definition.js';
35
+ export type { PackageNodeShape, PackageInputField, PackageOutputField, PackageNodeDefinition } from './core/nodes/package-definition.js';
36
+ /**
37
+ * Wrap a package factory's spec literal as the `FlowAction` instance `.step()`
38
+ * accepts, keeping the literal's own type. A built-in factory extends the SDK's
39
+ * `FlowActionSpec` union by augmenting an internal interface; a package has no
40
+ * such access, and does not need it: `.step(name, spec)` takes any `FlowAction`,
41
+ * and this is the one way to make one. The returned value IS the spec (the
42
+ * serializer reads `kind` and `inputs` off it) and is `instanceof FlowAction`.
43
+ *
44
+ * @param spec - The spec literal the factory built, `{ kind, inputs }`.
45
+ * @returns The same spec, typed as itself and as a `FlowAction`.
46
+ */
47
+ export declare function packageActionSpec<S extends {
48
+ kind: string;
49
+ }>(spec: S): S & FlowAction;
50
+ /**
51
+ * The version of this extension contract. A package pins the version it was
52
+ * written against in `defineFlowNodeFamily({ contract })`; a mismatch is refused
53
+ * at import time, before any flow is built, with both numbers in the sentence.
54
+ * It moves only when a hook's signature changes.
55
+ */
56
+ export declare const EXTENSION_CONTRACT_VERSION = 1;
57
+ /** What a package passes to `defineFlowNodeFamily`. */
58
+ export interface FlowNodeFamilyDefinition {
59
+ /** Stable id, e.g. `'local'`. Unique across the registry. */
60
+ readonly id: string;
61
+ readonly title: string;
62
+ /** The family's node types: single-version (`FlowNodeType`) or versioned (`FlowVersionedNodeType`), as a built-in family lists them. */
63
+ readonly types: FlowNodeFamily['types'];
64
+ readonly diagnostics: DiagnosticCatalog;
65
+ /** Rules over every placement of this family's types, after the per-node checks. */
66
+ readonly check?: NodeFamily<FlowContexts>['check'];
67
+ /** The package the family comes from; written as provenance into every definition. */
68
+ readonly package: PackageOrigin;
69
+ /** The `EXTENSION_CONTRACT_VERSION` the package was written against. */
70
+ readonly contract: number;
71
+ }
72
+ /**
73
+ * Define and register a node-type package's family with the Flow registry.
74
+ *
75
+ * Returns the family. Refuses, by throwing at import time, a contract version
76
+ * other than this SDK's and a family with no package name. The registry refuses,
77
+ * when it adopts the family (at compile time, before any node is serialized), a
78
+ * kind, type name, prefix or diagnostic code a built-in family or an earlier
79
+ * package owns, and a type that does not declare `definition: 'package'`.
80
+ */
81
+ export declare function defineFlowNodeFamily(definition: FlowNodeFamilyDefinition): FlowNodeFamily;
package/dist/extend.js ADDED
@@ -0,0 +1,56 @@
1
+ import { contributeFamily } from './core/nodes/extensions.js';
2
+ import { builtinActionSpec } from './core/actions.js';
3
+ export { defineNodeDefinition, DEFAULT_PACKAGE_DEFINITION_VERSION, } from './core/nodes/package-definition.js';
4
+ /**
5
+ * Wrap a package factory's spec literal as the `FlowAction` instance `.step()`
6
+ * accepts, keeping the literal's own type. A built-in factory extends the SDK's
7
+ * `FlowActionSpec` union by augmenting an internal interface; a package has no
8
+ * such access, and does not need it: `.step(name, spec)` takes any `FlowAction`,
9
+ * and this is the one way to make one. The returned value IS the spec (the
10
+ * serializer reads `kind` and `inputs` off it) and is `instanceof FlowAction`.
11
+ *
12
+ * @param spec - The spec literal the factory built, `{ kind, inputs }`.
13
+ * @returns The same spec, typed as itself and as a `FlowAction`.
14
+ */
15
+ export function packageActionSpec(spec) {
16
+ return builtinActionSpec(spec);
17
+ }
18
+ /**
19
+ * The version of this extension contract. A package pins the version it was
20
+ * written against in `defineFlowNodeFamily({ contract })`; a mismatch is refused
21
+ * at import time, before any flow is built, with both numbers in the sentence.
22
+ * It moves only when a hook's signature changes.
23
+ */
24
+ export const EXTENSION_CONTRACT_VERSION = 1;
25
+ /**
26
+ * Define and register a node-type package's family with the Flow registry.
27
+ *
28
+ * Returns the family. Refuses, by throwing at import time, a contract version
29
+ * other than this SDK's and a family with no package name. The registry refuses,
30
+ * when it adopts the family (at compile time, before any node is serialized), a
31
+ * kind, type name, prefix or diagnostic code a built-in family or an earlier
32
+ * package owns, and a type that does not declare `definition: 'package'`.
33
+ */
34
+ export function defineFlowNodeFamily(definition) {
35
+ if (definition.contract !== EXTENSION_CONTRACT_VERSION) {
36
+ throw new Error(`node-type package ${definition.package?.name ?? definition.id} was written against extension contract `
37
+ + `${String(definition.contract)}, but this @uipath/maestro-builder-sdk provides ${EXTENSION_CONTRACT_VERSION}. `
38
+ + 'Update the package (or the SDK) so the two agree.');
39
+ }
40
+ if (typeof definition.package?.name !== 'string' || definition.package.name === '') {
41
+ throw new Error(`node-type family "${definition.id}" names no package; defineFlowNodeFamily needs package.name.`);
42
+ }
43
+ const family = {
44
+ id: definition.id,
45
+ title: definition.title,
46
+ types: definition.types,
47
+ diagnostics: definition.diagnostics,
48
+ package: { name: definition.package.name, ...(definition.package.version !== undefined ? { version: definition.package.version } : {}) },
49
+ ...(definition.check !== undefined ? { check: definition.check } : {}),
50
+ };
51
+ // Contributed, not registered: this entry stays light at import (FB009), so it
52
+ // never loads the Flow registry; the registry adopts the family when it loads
53
+ // (`core/nodes/extensions.ts`), and registers it at once if it already has.
54
+ contributeFamily(family);
55
+ return family;
56
+ }
@@ -11,3 +11,10 @@ import type { FlowContexts, FlowNodeFamily } from './flow-node-type.js';
11
11
  /** The families, in no significant order. Adding one is the whole of registering it. */
12
12
  export declare const FLOW_NODE_FAMILIES: readonly FlowNodeFamily[];
13
13
  export declare const FLOW_NODE_TYPES: NodeTypeRegistry<FlowContexts>;
14
+ /**
15
+ * Register a node-type package's family with the Flow registry
16
+ * (`@uipath/maestro-builder-sdk/extend`'s `defineFlowNodeFamily` calls this).
17
+ * Refused, with the package named, when the family collides with a built-in
18
+ * family or with a package registered earlier.
19
+ */
20
+ export declare function registerFlowNodeFamily(family: FlowNodeFamily): FlowNodeFamily;
@@ -7,6 +7,7 @@
7
7
  * is left to port.
8
8
  */
9
9
  import { NodeTypeRegistry } from './core/nodes/registry.js';
10
+ import { adoptContributedFamilies } from './core/nodes/extensions.js';
10
11
  import { hasBundledDefinition } from './flow-definitions.js';
11
12
  import { agentFlowFamily } from './nodes/agent/flow/family.js';
12
13
  import { aiPatternsFlowFamily } from './nodes/ai-patterns/flow/family.js';
@@ -25,3 +26,16 @@ import { transformFlowFamily } from './nodes/transform/flow/family.js';
25
26
  /** The families, in no significant order. Adding one is the whole of registering it. */
26
27
  export const FLOW_NODE_FAMILIES = [agentFlowFamily, aiPatternsFlowFamily, connectorFlowFamily, conversationFlowFamily, dataFabricFlowFamily, delayFlowFamily, documentFlowFamily, hitlFlowFamily, httpFlowFamily, mockFlowFamily, processFlowFamily, queueFlowFamily, scriptFlowFamily, transformFlowFamily];
27
28
  export const FLOW_NODE_TYPES = new NodeTypeRegistry('flow', FLOW_NODE_FAMILIES, { hasBundledDefinition });
29
+ // Node-type packages contribute their families through the light `/extend` entry;
30
+ // adopt the ones already contributed and register every later one as it arrives.
31
+ adoptContributedFamilies((family) => FLOW_NODE_TYPES.register(family));
32
+ /**
33
+ * Register a node-type package's family with the Flow registry
34
+ * (`@uipath/maestro-builder-sdk/extend`'s `defineFlowNodeFamily` calls this).
35
+ * Refused, with the package named, when the family collides with a built-in
36
+ * family or with a package registered earlier.
37
+ */
38
+ export function registerFlowNodeFamily(family) {
39
+ FLOW_NODE_TYPES.register(family);
40
+ return family;
41
+ }
package/dist/merge-cli.js CHANGED
@@ -49,11 +49,12 @@ import { MergeRefusedError, mergeFlowWithSidecars } from './merge.js';
49
49
  import { sidecarSetFromList } from './merge-sidecars.js';
50
50
  import { isWithin } from './path-within.js';
51
51
  import { resourceSourcesOfOtherFlows } from './project-resource-users.js';
52
- import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
52
+ import { flagValue, flagValues, runWhenInvokedDirectly } from './cli-run.js';
53
53
  import { ensureTypeScriptRuntime } from './node-runtime.js';
54
54
  import { flowCheckOpts, openCliLibrary } from './cli-library.js';
55
55
  import { Bindings } from './bindings.js';
56
56
  import { defaultBindingsFile } from './workdir.js';
57
+ import { loadNodePackages } from './node-packages.js';
57
58
  import { baselineFromOriginal, checkEdited, classifyDrafts, compileSourceDraftTolerant, findingCounts, splitUnplaced } from './merge-compile.js';
58
59
  const USAGE = 'usage: flow-merge <original.flow> <edited.flow | Edited.flow.ts> [-o merged.flow] '
59
60
  + '[--baseline <baseline.flow> | --baseline-from <original.flow>] [--library <dir>] [--bindings <file>] [--strict]';
@@ -159,7 +160,7 @@ export async function run(argv) {
159
160
  console.error(USAGE);
160
161
  return 2;
161
162
  }
162
- const flags = new Set(['-o', '--baseline', '--baseline-from', '--library', '--bindings', '--connectors-local']);
163
+ const flags = new Set(['-o', '--baseline', '--baseline-from', '--library', '--bindings', '--connectors-local', '--package']);
163
164
  const positionals = argv.filter((a, i) => !flags.has(a) && !flags.has(argv[i - 1]) && !a.startsWith('-'));
164
165
  const [originalPath, editedPath] = positionals;
165
166
  if (!originalPath || !editedPath) {
@@ -200,6 +201,15 @@ export async function run(argv) {
200
201
  else {
201
202
  edited = JSON.parse(readFileSync(editedPath, 'utf8'));
202
203
  }
204
+ if (baselineFrom) {
205
+ // The baseline is a decompile of the original: load the node-type packages
206
+ // its definitions name, under the project's trust (package.json nodePackages
207
+ // or --package), so the decompiled source imports them rather than printing
208
+ // rawNode(...) — which would differ from the edit and refuse a clean merge.
209
+ const packages = await loadNodePackages(baselineFrom, JSON.parse(readFileSync(baselineFrom, 'utf8')), flagValues(argv, '--package'));
210
+ for (const message of packages.warnings)
211
+ console.error(`flow-merge: warning: ${message}`);
212
+ }
203
213
  const baselineCompile = baselineFrom
204
214
  ? await baselineFromOriginal(baselineFrom, compileOpts, (m) => console.error(`flow-merge: decompile warning: ${m}`))
205
215
  : undefined;
@@ -49,7 +49,12 @@ export async function baselineFromOriginal(originalPath, opts = {}, warn) {
49
49
  }
50
50
  },
51
51
  });
52
- const dir = mkdtempSync(join(tmpdir(), 'flow-merge-baseline-'));
52
+ // Beside the original, not in the system temp dir: the decompiled source imports
53
+ // the SDK, which `useThisSdkForAuthoredSource` resolves from anywhere, and any
54
+ // NODE-TYPE PACKAGE the original was written with, which Node resolves only by
55
+ // walking up from the file to a `node_modules` — the author's project's. A
56
+ // directory that cannot be written to (a read-only mount) falls back to tmp.
57
+ const dir = baselineDirectory(originalPath);
53
58
  try {
54
59
  writeFileSync(join(dir, 'package.json'), '{"type":"module"}\n');
55
60
  const file = join(dir, 'Baseline.flow.ts');
@@ -60,6 +65,14 @@ export async function baselineFromOriginal(originalPath, opts = {}, warn) {
60
65
  rmSync(dir, { recursive: true, force: true });
61
66
  }
62
67
  }
68
+ function baselineDirectory(originalPath) {
69
+ try {
70
+ return mkdtempSync(join(dirname(resolve(originalPath)), '.flow-merge-baseline-'));
71
+ }
72
+ catch {
73
+ return mkdtempSync(join(tmpdir(), 'flow-merge-baseline-'));
74
+ }
75
+ }
63
76
  /**
64
77
  * Sort each finding by what merge does with its node: `refused` when the node
65
78
  * comes from the edited flow (reason `edited`: the author changed or added it;