@uipath/packager-tool-workflowcompiler 1.202.0-preview.141 → 1.202.0-preview.142

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.
@@ -1,530 +0,0 @@
1
- import { type ChildProcess, spawn } from "node:child_process";
2
- import {
3
- type IFileSystem,
4
- type IToolLogger,
5
- ToolErrorCodes,
6
- ToolResult,
7
- translate,
8
- } from "@uipath/solutionpackager-tool-core";
9
- import type {
10
- IOutputLog,
11
- IOutputMessage,
12
- IOutputProgress,
13
- IOutputResult,
14
- } from "./output-entry.js";
15
- import { workflowCompilerConfig } from "./workflow-compiler-config.js";
16
- import {
17
- type IWorkflowCompilerPathResolver,
18
- WorkflowCompilerPathResolver,
19
- } from "./workflow-compiler-path-resolver.js";
20
-
21
- /** Signals when each child-process stream has finished. */
22
- interface StreamCompletion {
23
- markStdoutEnded(): void;
24
- markStderrEnded(): void;
25
- markProcessExited(): void;
26
- }
27
-
28
- function camelCaseKeys(obj: unknown): unknown {
29
- if (Array.isArray(obj)) {
30
- return obj.map(camelCaseKeys);
31
- }
32
- if (obj !== null && typeof obj === "object") {
33
- const result: Record<string, unknown> = {};
34
- for (const [key, value] of Object.entries(obj)) {
35
- result[key.charAt(0).toLowerCase() + key.slice(1)] =
36
- camelCaseKeys(value);
37
- }
38
- return result;
39
- }
40
- return obj;
41
- }
42
-
43
- /**
44
- * Executor for running workflow compiler commands as child processes.
45
- * Handles process spawning, output parsing, and logging.
46
- */
47
- export class WorkflowCompilerExecutor {
48
- private readonly pathResolver: IWorkflowCompilerPathResolver;
49
-
50
- constructor(
51
- private readonly logger: IToolLogger,
52
- fileSystem: IFileSystem,
53
- ) {
54
- this.pathResolver = WorkflowCompilerPathResolver.getInstance(
55
- logger,
56
- fileSystem,
57
- );
58
- }
59
-
60
- /**
61
- * Execute a workflow compiler command
62
- * @param operation - Operation name (e.g., 'restore', 'build', 'pack', 'validate')
63
- * @param args - Additional command arguments
64
- * @param cancellationToken - Optional cancellation token
65
- * @returns Promise resolving to ToolResult
66
- */
67
- async executeAsync(
68
- operation: string,
69
- args: string[],
70
- cancellationToken?: AbortSignal,
71
- ): Promise<ToolResult> {
72
- const startTime = Date.now();
73
- const compilerPath = await this.pathResolver.getCompilerPathAsync();
74
- // The compiler ships as a managed .dll, so it always runs via a dotnet host. `dotnetPath`
75
- // defaults to `dotnet` on PATH; consumers with a bundled runtime (rpa-tool) point it at that
76
- // muxer so the compiler runs under the bundled .NET instead of requiring a machine-wide install.
77
- const executable = workflowCompilerConfig.dotnetPath;
78
- const commandArgs = [compilerPath, operation, ...args];
79
-
80
- this.logCommandStart(operation, executable, commandArgs);
81
-
82
- return new Promise((resolve) => {
83
- let result: IOutputResult | null = null;
84
- const fallbackErrors: string[] = [];
85
- const analyzerFindings: Record<string, unknown>[] = [];
86
-
87
- const childProcess = this.spawnProcess(
88
- executable,
89
- commandArgs,
90
- workflowCompilerConfig.dotnetEnv,
91
- );
92
-
93
- // Track stream completion
94
- let stdoutEnded = false;
95
- let stderrEnded = false;
96
- let processExited = false;
97
-
98
- const checkComplete = () => {
99
- if (stdoutEnded && stderrEnded && processExited) {
100
- const elapsed = Date.now() - startTime;
101
-
102
- if (result) {
103
- result.elapsed = elapsed;
104
- this.logger.info(
105
- translate.t(
106
- "toolWorkflowcompiler.lifecycle.completed",
107
- {
108
- operation,
109
- elapsed: elapsed.toString(),
110
- errorCode: result.errorCode,
111
- },
112
- ),
113
- );
114
- const toolResult = new ToolResult(
115
- result.errorCode,
116
- result.message,
117
- result.outputPackages,
118
- );
119
- toolResult.details = withAnalyzerFindings(
120
- getAdditionalResultDetails(result),
121
- analyzerFindings,
122
- );
123
- resolve(toolResult);
124
- } else {
125
- const exitCode = childProcess.exitCode ?? -1;
126
- const errorMessage =
127
- fallbackErrors.join("\n") ||
128
- translate.t(
129
- "toolWorkflowcompiler.errors.processExited",
130
- {
131
- exitCode: exitCode.toString(),
132
- },
133
- );
134
- this.logger.error(
135
- translate.t("toolWorkflowcompiler.errors.failed", {
136
- operation,
137
- errorMessage,
138
- }),
139
- );
140
- resolve(
141
- new ToolResult(
142
- ToolErrorCodes.InternalError,
143
- errorMessage,
144
- [],
145
- ),
146
- );
147
- }
148
- }
149
- };
150
-
151
- const streamCompletion = {
152
- markStdoutEnded: () => {
153
- stdoutEnded = true;
154
- checkComplete();
155
- },
156
- markStderrEnded: () => {
157
- stderrEnded = true;
158
- checkComplete();
159
- },
160
- markProcessExited: () => {
161
- processExited = true;
162
- checkComplete();
163
- },
164
- };
165
-
166
- this.setupStdoutHandling(
167
- childProcess,
168
- streamCompletion,
169
- (r) => (result = r),
170
- analyzerFindings,
171
- );
172
- this.setupStderrHandling(
173
- childProcess,
174
- streamCompletion,
175
- fallbackErrors,
176
- analyzerFindings,
177
- );
178
- this.setupProcessEventHandlers(
179
- childProcess,
180
- streamCompletion,
181
- resolve,
182
- );
183
- this.setupCancellationHandler(
184
- childProcess,
185
- cancellationToken,
186
- resolve,
187
- );
188
- });
189
- }
190
-
191
- protected spawnProcess(
192
- command: string,
193
- args: string[],
194
- env?: Record<string, string>,
195
- ) {
196
- return spawn(command, args, {
197
- stdio: ["ignore", "pipe", "pipe"],
198
- shell: false,
199
- windowsHide: true,
200
- // Undefined inherits the parent env; a provided env is merged over it (so DOTNET_ROOT
201
- // et al. are added without dropping PATH and friends the child still needs).
202
- env: env ? { ...process.env, ...env } : undefined,
203
- });
204
- }
205
-
206
- private logCommandStart(
207
- operation: string,
208
- executable: string,
209
- commandArgs: string[],
210
- ): void {
211
- const fullCommand = [executable, ...commandArgs];
212
- this.logger.info(
213
- translate.t("toolWorkflowcompiler.lifecycle.started", {
214
- operation,
215
- }),
216
- );
217
- this.logger.info(
218
- translate.t("toolWorkflowcompiler.lifecycle.command", {
219
- command: fullCommand.join(" "),
220
- }),
221
- );
222
- }
223
-
224
- private setupStdoutHandling(
225
- childProcess: ChildProcess,
226
- streamCompletion: StreamCompletion,
227
- onResult: (result: IOutputResult) => void,
228
- analyzerFindings: Record<string, unknown>[],
229
- ): void {
230
- if (!childProcess.stdout) {
231
- streamCompletion.markStdoutEnded();
232
- return;
233
- }
234
-
235
- let buffer = "";
236
- childProcess.stdout.on("data", (data: Buffer) => {
237
- buffer += data.toString();
238
- const lines = buffer.split("\n");
239
- // Keep the last element — it's either a partial line or ""
240
- buffer = lines.pop()!;
241
- for (const line of lines) {
242
- if (!line.trim()) continue;
243
- this.processOutputLine(line, onResult, analyzerFindings);
244
- }
245
- });
246
-
247
- childProcess.stdout.on("end", () => {
248
- if (buffer.trim()) {
249
- this.processOutputLine(buffer, onResult, analyzerFindings);
250
- }
251
- streamCompletion.markStdoutEnded();
252
- });
253
- }
254
-
255
- private processOutputLine(
256
- line: string,
257
- onResult: (result: IOutputResult) => void,
258
- analyzerFindings: Record<string, unknown>[],
259
- ): void {
260
- const entry = this.parseOutputEntry(line);
261
- if (!entry) return;
262
-
263
- if (entry.type === "Result") {
264
- onResult(entry as IOutputResult);
265
- } else if (entry.type === "Log") {
266
- const log = entry as IOutputLog;
267
- const finding = toAnalyzerFinding(log);
268
- if (finding) {
269
- analyzerFindings.push(finding);
270
- }
271
- this.logMessage(log);
272
- } else if (entry.type === "Progress") {
273
- this.logProgress(entry as IOutputProgress);
274
- }
275
- }
276
-
277
- private logMessage(log: IOutputLog): void {
278
- switch (log.logLevel) {
279
- case "Information":
280
- this.logger.info(log.message);
281
- break;
282
- case "Warning":
283
- this.logger.warn(log.message);
284
- break;
285
- case "Error":
286
- this.logger.error(log.message);
287
- break;
288
- }
289
- }
290
-
291
- private logProgress(progress: IOutputProgress): void {
292
- const percent = progress.percentage ? `${progress.percentage}%` : "";
293
- const separator = percent && progress.message ? " - " : "";
294
- this.logger.progress(`${percent}${separator}${progress.message ?? ""}`);
295
- }
296
-
297
- private setupStderrHandling(
298
- childProcess: ChildProcess,
299
- streamCompletion: StreamCompletion,
300
- fallbackErrors: string[],
301
- analyzerFindings: Record<string, unknown>[],
302
- ): void {
303
- if (!childProcess.stderr) {
304
- streamCompletion.markStderrEnded();
305
- return;
306
- }
307
-
308
- let buffer = "";
309
- childProcess.stderr.on("data", (data: Buffer) => {
310
- buffer += data.toString();
311
- const lines = buffer.split("\n");
312
- buffer = lines.pop()!;
313
- for (const line of lines) {
314
- if (!line.trim()) continue;
315
- this.processErrorLine(line, fallbackErrors, analyzerFindings);
316
- }
317
- });
318
-
319
- childProcess.stderr.on("end", () => {
320
- if (buffer.trim()) {
321
- this.processErrorLine(buffer, fallbackErrors, analyzerFindings);
322
- }
323
- streamCompletion.markStderrEnded();
324
- });
325
- }
326
-
327
- private processErrorLine(
328
- line: string,
329
- fallbackErrors: string[],
330
- analyzerFindings: Record<string, unknown>[],
331
- ): void {
332
- const entry = this.parseOutputEntry(line);
333
-
334
- if (entry && entry.type === "Log") {
335
- const log = entry as IOutputLog;
336
- const finding = toAnalyzerFinding(log);
337
- if (finding) {
338
- analyzerFindings.push(finding);
339
- }
340
- this.logger.error(log.message);
341
- } else {
342
- fallbackErrors.push(line);
343
- this.logger.error(line);
344
- }
345
- }
346
-
347
- private setupProcessEventHandlers(
348
- childProcess: ChildProcess,
349
- streamCompletion: StreamCompletion,
350
- resolve: (value: ToolResult) => void,
351
- ): void {
352
- childProcess.on("exit", () => streamCompletion.markProcessExited());
353
-
354
- childProcess.on("error", (error: Error) => {
355
- this.logger.error(
356
- translate.t("toolWorkflowcompiler.errors.processError", {
357
- message: error.message,
358
- }),
359
- );
360
- resolve(
361
- new ToolResult(
362
- ToolErrorCodes.InternalError,
363
- translate.t("toolWorkflowcompiler.errors.startFailed", {
364
- message: error.message,
365
- }),
366
- [],
367
- ),
368
- );
369
- });
370
- }
371
-
372
- private setupCancellationHandler(
373
- childProcess: ChildProcess,
374
- cancellationToken: AbortSignal | undefined,
375
- resolve: (value: ToolResult) => void,
376
- ): void {
377
- if (!cancellationToken) return;
378
-
379
- cancellationToken.addEventListener("abort", () => {
380
- this.logger.warn(
381
- translate.t("toolWorkflowcompiler.lifecycle.cancelled"),
382
- );
383
- childProcess.kill("SIGTERM");
384
- resolve(
385
- new ToolResult(
386
- ToolErrorCodes.Canceled,
387
- translate.t(
388
- "toolWorkflowcompiler.errors.operationCancelled",
389
- ),
390
- [],
391
- ),
392
- );
393
- });
394
- }
395
-
396
- private parseOutputEntry(line: string): IOutputMessage | null {
397
- try {
398
- const parsed = JSON.parse(line);
399
-
400
- // Convert PascalCase to camelCase
401
- const camelCased = camelCaseKeys(parsed) as Record<string, unknown>;
402
-
403
- // Convert numeric error codes to strings
404
- if (camelCased.errorCode !== undefined) {
405
- if (camelCased.errorCode === 0 || camelCased.success === true) {
406
- camelCased.errorCode = ToolErrorCodes.Success;
407
- } else {
408
- camelCased.errorCode = String(camelCased.errorCode);
409
- }
410
- }
411
-
412
- // Extract paths from OutputPackages array of objects
413
- if (camelCased.outputPackages) {
414
- camelCased.outputPackages = (
415
- camelCased.outputPackages as Array<{ path?: string }>
416
- ).map((pkg) => pkg.path || pkg);
417
- }
418
-
419
- return camelCased as unknown as IOutputMessage;
420
- } catch {
421
- // Not JSON, return null
422
- return null;
423
- }
424
- }
425
- }
426
-
427
- /**
428
- * Allow-list-driven copy of any *extra* fields the WorkflowCompiler emits on
429
- * its result envelope (e.g. analyzer `findings`) into `ToolResult.details`,
430
- * so downstream callers can read structured data without the executor having
431
- * to know what each consumer needs.
432
- *
433
- * Known envelope fields (already exposed as first-class on `ToolResult` or
434
- * intentionally internal) are excluded; anything else is copied through.
435
- * Adding the new field to `KNOWN_ENVELOPE_KEYS` is what keeps it out of
436
- * `details` — leaving it absent surfaces it, which is the safer default.
437
- */
438
- const KNOWN_ENVELOPE_KEYS = new Set<keyof IOutputResult>([
439
- "elapsed",
440
- "errorCode",
441
- "inputId",
442
- "message",
443
- "outputPackages",
444
- "toolId",
445
- "type",
446
- ]);
447
-
448
- function getAdditionalResultDetails(
449
- result: IOutputResult,
450
- ): Record<string, unknown> | undefined {
451
- const details: Record<string, unknown> = {};
452
- for (const key of Object.keys(result)) {
453
- if (!KNOWN_ENVELOPE_KEYS.has(key as keyof IOutputResult)) {
454
- details[key] = (result as Record<string, unknown>)[key];
455
- }
456
- }
457
-
458
- return Object.keys(details).length > 0 ? details : undefined;
459
- }
460
-
461
- const ANALYZER_LOG_SOURCE = "Analyzer";
462
-
463
- /** Only a *populated* envelope array wins — `CommandResult` defaults its lists to `[]`. */
464
- function withAnalyzerFindings(
465
- details: Record<string, unknown> | undefined,
466
- findings: Record<string, unknown>[],
467
- ): Record<string, unknown> | undefined {
468
- const fromEnvelope = details?.findings;
469
- if (
470
- findings.length === 0 ||
471
- (Array.isArray(fromEnvelope) && fromEnvelope.length > 0)
472
- ) {
473
- return details;
474
- }
475
- return { ...details, findings };
476
- }
477
-
478
- /** Flattened because consumers match flat keys only; the payload omits null members. */
479
- function toAnalyzerFinding(
480
- log: IOutputLog,
481
- ): Record<string, unknown> | undefined {
482
- if (log.source !== ANALYZER_LOG_SOURCE) {
483
- return undefined;
484
- }
485
-
486
- const message = parseAnalyzerMessage(log.data) ?? {};
487
- const activityId = message.ActivityId as { IdRef?: string } | undefined;
488
- const item = message.Item as { Name?: string; Type?: string } | undefined;
489
- return {
490
- ...message,
491
- ErrorCode: message.ErrorCode ?? log.code,
492
- Description: message.Description ?? log.message,
493
- FilePath: message.FilePath ?? log.filePath,
494
- ErrorSeverity: message.ErrorSeverity ?? toErrorSeverity(log.logLevel),
495
- ActivityIdRef: activityId?.IdRef ?? log.id,
496
- AnalyzerItemName: item?.Name,
497
- AnalyzerItemType: item?.Type,
498
- };
499
- }
500
-
501
- /** Undo the compiler's `TraceLevel` → `LogLevel` mapping, so a consumer's lookup still matches. */
502
- function toErrorSeverity(logLevel: IOutputLog["logLevel"]): string {
503
- switch (logLevel) {
504
- case "Debug":
505
- case "Trace":
506
- return "Verbose";
507
- case "Information":
508
- return "Info";
509
- default:
510
- return logLevel;
511
- }
512
- }
513
-
514
- function parseAnalyzerMessage(
515
- data: string | undefined,
516
- ): Record<string, unknown> | undefined {
517
- if (!data) {
518
- return undefined;
519
- }
520
- try {
521
- const parsed: unknown = JSON.parse(data);
522
- return typeof parsed === "object" &&
523
- parsed !== null &&
524
- !Array.isArray(parsed)
525
- ? (parsed as Record<string, unknown>)
526
- : undefined;
527
- } catch {
528
- return undefined;
529
- }
530
- }