@uipath/packager-tool-workflowcompiler 0.0.14

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.
@@ -0,0 +1,386 @@
1
+ import { 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 {
16
+ type IWorkflowCompilerPathResolver,
17
+ WorkflowCompilerPathResolver,
18
+ } from "./workflow-compiler-path-resolver.js";
19
+
20
+ function camelCaseKeys(obj: unknown): unknown {
21
+ if (Array.isArray(obj)) {
22
+ return obj.map(camelCaseKeys);
23
+ }
24
+ if (obj !== null && typeof obj === "object") {
25
+ const result: Record<string, unknown> = {};
26
+ for (const [key, value] of Object.entries(obj)) {
27
+ result[key.charAt(0).toLowerCase() + key.slice(1)] =
28
+ camelCaseKeys(value);
29
+ }
30
+ return result;
31
+ }
32
+ return obj;
33
+ }
34
+
35
+ /**
36
+ * Executor for running workflow compiler commands as child processes.
37
+ * Handles process spawning, output parsing, and logging.
38
+ */
39
+ export class WorkflowCompilerExecutor {
40
+ private static readonly DOTNET_EXECUTABLE = "dotnet";
41
+ private readonly pathResolver: IWorkflowCompilerPathResolver;
42
+
43
+ constructor(
44
+ private readonly logger: IToolLogger,
45
+ fileSystem: IFileSystem,
46
+ ) {
47
+ this.pathResolver = WorkflowCompilerPathResolver.getInstance(
48
+ logger,
49
+ fileSystem,
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Execute a workflow compiler command
55
+ * @param operation - Operation name (e.g., 'restore', 'build', 'pack', 'validate')
56
+ * @param args - Additional command arguments
57
+ * @param cancellationToken - Optional cancellation token
58
+ * @returns Promise resolving to ToolResult
59
+ */
60
+ async executeAsync(
61
+ operation: string,
62
+ args: string[],
63
+ cancellationToken?: AbortSignal,
64
+ ): Promise<ToolResult> {
65
+ const startTime = Date.now();
66
+ const compilerPath = await this.pathResolver.getCompilerPathAsync();
67
+ const isDll = compilerPath.endsWith(".dll");
68
+ const executable = isDll
69
+ ? WorkflowCompilerExecutor.DOTNET_EXECUTABLE
70
+ : compilerPath;
71
+ const commandArgs = isDll
72
+ ? [compilerPath, operation, ...args]
73
+ : [operation, ...args];
74
+
75
+ this.logCommandStart(operation, executable, commandArgs);
76
+
77
+ return new Promise((resolve) => {
78
+ let result: IOutputResult | null = null;
79
+ const fallbackErrors: string[] = [];
80
+
81
+ const childProcess = this.spawnProcess(executable, commandArgs);
82
+
83
+ // Track stream completion
84
+ let stdoutEnded = false;
85
+ let stderrEnded = false;
86
+ let processExited = false;
87
+
88
+ const checkComplete = () => {
89
+ if (stdoutEnded && stderrEnded && processExited) {
90
+ const elapsed = Date.now() - startTime;
91
+
92
+ if (result) {
93
+ result.elapsed = elapsed;
94
+ this.logger.info(
95
+ translate.t(
96
+ "toolWorkflowcompiler.lifecycle.completed",
97
+ {
98
+ operation,
99
+ elapsed: elapsed.toString(),
100
+ errorCode: result.errorCode,
101
+ },
102
+ ),
103
+ );
104
+ resolve(
105
+ new ToolResult(
106
+ result.errorCode,
107
+ result.message,
108
+ result.outputPackages,
109
+ ),
110
+ );
111
+ } else {
112
+ const exitCode = childProcess.exitCode ?? -1;
113
+ const errorMessage =
114
+ fallbackErrors.join("\n") ||
115
+ translate.t(
116
+ "toolWorkflowcompiler.errors.processExited",
117
+ {
118
+ exitCode: exitCode.toString(),
119
+ },
120
+ );
121
+ this.logger.error(
122
+ translate.t("toolWorkflowcompiler.errors.failed", {
123
+ operation,
124
+ errorMessage,
125
+ }),
126
+ );
127
+ resolve(
128
+ new ToolResult(
129
+ ToolErrorCodes.InternalError,
130
+ errorMessage,
131
+ [],
132
+ ),
133
+ );
134
+ }
135
+ }
136
+ };
137
+
138
+ const streamCompletion = {
139
+ markStdoutEnded: () => {
140
+ stdoutEnded = true;
141
+ checkComplete();
142
+ },
143
+ markStderrEnded: () => {
144
+ stderrEnded = true;
145
+ checkComplete();
146
+ },
147
+ markProcessExited: () => {
148
+ processExited = true;
149
+ checkComplete();
150
+ },
151
+ };
152
+
153
+ this.setupStdoutHandling(
154
+ childProcess,
155
+ streamCompletion,
156
+ (r) => (result = r),
157
+ );
158
+ this.setupStderrHandling(
159
+ childProcess,
160
+ streamCompletion,
161
+ fallbackErrors,
162
+ );
163
+ this.setupProcessEventHandlers(
164
+ childProcess,
165
+ streamCompletion,
166
+ resolve,
167
+ );
168
+ this.setupCancellationHandler(
169
+ childProcess,
170
+ cancellationToken,
171
+ resolve,
172
+ );
173
+ });
174
+ }
175
+
176
+ protected spawnProcess(command: string, args: string[]) {
177
+ return spawn(command, args, {
178
+ stdio: ["ignore", "pipe", "pipe"],
179
+ shell: false,
180
+ windowsHide: true,
181
+ });
182
+ }
183
+
184
+ private logCommandStart(
185
+ operation: string,
186
+ executable: string,
187
+ commandArgs: string[],
188
+ ): void {
189
+ const fullCommand = [executable, ...commandArgs];
190
+ this.logger.info(
191
+ translate.t("toolWorkflowcompiler.lifecycle.started", {
192
+ operation,
193
+ }),
194
+ );
195
+ this.logger.info(
196
+ translate.t("toolWorkflowcompiler.lifecycle.command", {
197
+ command: fullCommand.join(" "),
198
+ }),
199
+ );
200
+ }
201
+
202
+ private setupStdoutHandling(
203
+ childProcess: any,
204
+ streamCompletion: any,
205
+ onResult: (result: IOutputResult) => void,
206
+ ): void {
207
+ if (!childProcess.stdout) {
208
+ streamCompletion.markStdoutEnded();
209
+ return;
210
+ }
211
+
212
+ let buffer = "";
213
+ childProcess.stdout.on("data", (data: Buffer) => {
214
+ buffer += data.toString();
215
+ const lines = buffer.split("\n");
216
+ // Keep the last element — it's either a partial line or ""
217
+ buffer = lines.pop()!;
218
+ for (const line of lines) {
219
+ if (!line.trim()) continue;
220
+ this.processOutputLine(line, onResult);
221
+ }
222
+ });
223
+
224
+ childProcess.stdout.on("end", () => {
225
+ if (buffer.trim()) {
226
+ this.processOutputLine(buffer, onResult);
227
+ }
228
+ streamCompletion.markStdoutEnded();
229
+ });
230
+ }
231
+
232
+ private processOutputLine(
233
+ line: string,
234
+ onResult: (result: IOutputResult) => void,
235
+ ): void {
236
+ const entry = this.parseOutputEntry(line);
237
+ if (!entry) return;
238
+
239
+ if (entry.type === "Result") {
240
+ onResult(entry as IOutputResult);
241
+ } else if (entry.type === "Log") {
242
+ this.logMessage(entry as IOutputLog);
243
+ } else if (entry.type === "Progress") {
244
+ this.logProgress(entry as IOutputProgress);
245
+ }
246
+ }
247
+
248
+ private logMessage(log: IOutputLog): void {
249
+ switch (log.logLevel) {
250
+ case "Information":
251
+ this.logger.info(log.message);
252
+ break;
253
+ case "Warning":
254
+ this.logger.warn(log.message);
255
+ break;
256
+ case "Error":
257
+ this.logger.error(log.message);
258
+ break;
259
+ }
260
+ }
261
+
262
+ private logProgress(progress: IOutputProgress): void {
263
+ const percent = progress.percentage ? `${progress.percentage}%` : "";
264
+ const separator = percent && progress.message ? " - " : "";
265
+ this.logger.progress(`${percent}${separator}${progress.message ?? ""}`);
266
+ }
267
+
268
+ private setupStderrHandling(
269
+ childProcess: any,
270
+ streamCompletion: any,
271
+ fallbackErrors: string[],
272
+ ): void {
273
+ if (!childProcess.stderr) {
274
+ streamCompletion.markStderrEnded();
275
+ return;
276
+ }
277
+
278
+ let buffer = "";
279
+ childProcess.stderr.on("data", (data: Buffer) => {
280
+ buffer += data.toString();
281
+ const lines = buffer.split("\n");
282
+ buffer = lines.pop()!;
283
+ for (const line of lines) {
284
+ if (!line.trim()) continue;
285
+ this.processErrorLine(line, fallbackErrors);
286
+ }
287
+ });
288
+
289
+ childProcess.stderr.on("end", () => {
290
+ if (buffer.trim()) {
291
+ this.processErrorLine(buffer, fallbackErrors);
292
+ }
293
+ streamCompletion.markStderrEnded();
294
+ });
295
+ }
296
+
297
+ private processErrorLine(line: string, fallbackErrors: string[]): void {
298
+ const entry = this.parseOutputEntry(line);
299
+
300
+ if (entry && entry.type === "Log") {
301
+ this.logger.error((entry as IOutputLog).message);
302
+ } else {
303
+ fallbackErrors.push(line);
304
+ this.logger.error(line);
305
+ }
306
+ }
307
+
308
+ private setupProcessEventHandlers(
309
+ childProcess: any,
310
+ streamCompletion: any,
311
+ resolve: (value: ToolResult) => void,
312
+ ): void {
313
+ childProcess.on("exit", () => streamCompletion.markProcessExited());
314
+
315
+ childProcess.on("error", (error: Error) => {
316
+ this.logger.error(
317
+ translate.t("toolWorkflowcompiler.errors.processError", {
318
+ message: error.message,
319
+ }),
320
+ );
321
+ resolve(
322
+ new ToolResult(
323
+ ToolErrorCodes.InternalError,
324
+ translate.t("toolWorkflowcompiler.errors.startFailed", {
325
+ message: error.message,
326
+ }),
327
+ [],
328
+ ),
329
+ );
330
+ });
331
+ }
332
+
333
+ private setupCancellationHandler(
334
+ childProcess: any,
335
+ cancellationToken: AbortSignal | undefined,
336
+ resolve: (value: ToolResult) => void,
337
+ ): void {
338
+ if (!cancellationToken) return;
339
+
340
+ cancellationToken.addEventListener("abort", () => {
341
+ this.logger.warn(
342
+ translate.t("toolWorkflowcompiler.lifecycle.cancelled"),
343
+ );
344
+ childProcess.kill("SIGTERM");
345
+ resolve(
346
+ new ToolResult(
347
+ ToolErrorCodes.Canceled,
348
+ translate.t(
349
+ "toolWorkflowcompiler.errors.operationCancelled",
350
+ ),
351
+ [],
352
+ ),
353
+ );
354
+ });
355
+ }
356
+
357
+ private parseOutputEntry(line: string): IOutputMessage | null {
358
+ try {
359
+ const parsed = JSON.parse(line);
360
+
361
+ // Convert PascalCase to camelCase
362
+ const camelCased = camelCaseKeys(parsed) as Record<string, any>;
363
+
364
+ // Convert numeric error codes to strings
365
+ if (camelCased.errorCode !== undefined) {
366
+ if (camelCased.errorCode === 0 || camelCased.success === true) {
367
+ camelCased.errorCode = ToolErrorCodes.Success;
368
+ } else {
369
+ camelCased.errorCode = String(camelCased.errorCode);
370
+ }
371
+ }
372
+
373
+ // Extract paths from OutputPackages array of objects
374
+ if (camelCased.outputPackages) {
375
+ camelCased.outputPackages = camelCased.outputPackages.map(
376
+ (pkg: any) => pkg.path || pkg,
377
+ );
378
+ }
379
+
380
+ return camelCased as IOutputMessage;
381
+ } catch {
382
+ // Not JSON, return null
383
+ return null;
384
+ }
385
+ }
386
+ }
@@ -0,0 +1,311 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { homedir, platform } from "node:os";
3
+ import type {
4
+ IFileSystem,
5
+ IToolLogger,
6
+ } from "@uipath/solutionpackager-tool-core";
7
+ import {
8
+ Path,
9
+ TemporaryStorageService,
10
+ translate,
11
+ } from "@uipath/solutionpackager-tool-core";
12
+ import { workflowCompilerConfig } from "./workflow-compiler-config.js";
13
+
14
+ const NUGET_FEED_URL =
15
+ "https://uipath.pkgs.visualstudio.com/Public.Feeds/_packaging/UiPath-Internal/nuget/v3/index.json";
16
+
17
+ const PLATFORM_PACKAGES: Record<
18
+ string,
19
+ { packageId: string; compilerFileName: string }
20
+ > = {
21
+ win32: {
22
+ packageId: "UiPath.WorkflowCompiler.Windows",
23
+ compilerFileName: "UiPath.WorkflowCompiler.exe",
24
+ },
25
+ darwin: {
26
+ packageId: "UiPath.WorkflowCompiler.macOS",
27
+ compilerFileName: "UiPath.WorkflowCompiler.dll",
28
+ },
29
+ linux: {
30
+ packageId: "UiPath.WorkflowCompiler.Linux",
31
+ compilerFileName: "UiPath.WorkflowCompiler.dll",
32
+ },
33
+ };
34
+
35
+ const RESTORE_CSPROJ_TEMPLATE = `<Project Sdk="Microsoft.NET.Sdk">
36
+ <PropertyGroup>
37
+ <TargetFramework>net8.0</TargetFramework>
38
+ </PropertyGroup>
39
+ <ItemGroup>
40
+ <PackageReference Include="{packageId}" Version="{version}" />
41
+ </ItemGroup>
42
+ </Project>`;
43
+
44
+ const EMPTY_NUGET_CONFIG = `<?xml version="1.0" encoding="utf-8"?>
45
+ <configuration>
46
+ </configuration>`;
47
+
48
+ export interface IWorkflowCompilerPathResolver {
49
+ getCompilerPathAsync(): Promise<string>;
50
+ }
51
+
52
+ /**
53
+ * Resolves and caches the workflow compiler executable path.
54
+ *
55
+ * Resolution chain:
56
+ * 1. In-memory cache (subsequent calls)
57
+ * 2. Explicit config path (`workflowCompilerConfig.workflowCompilerPath`)
58
+ * 3. `UIPATH_WORKFLOWCOMPILER_LOCATION` env var — full file path (pipelines / offline)
59
+ * 4. NuGet global packages cache
60
+ * 5. `dotnet restore` to populate NuGet cache, then (4) again
61
+ */
62
+ export class WorkflowCompilerPathResolver
63
+ implements IWorkflowCompilerPathResolver
64
+ {
65
+ private static instance: IWorkflowCompilerPathResolver | null = null;
66
+ private cachedPath: string | null = null;
67
+ private pendingResolution: Promise<string> | null = null;
68
+
69
+ private readonly tempStorage: TemporaryStorageService;
70
+
71
+ constructor(
72
+ private readonly logger: IToolLogger,
73
+ private readonly fileSystem: IFileSystem,
74
+ ) {
75
+ this.tempStorage = new TemporaryStorageService(fileSystem);
76
+ }
77
+
78
+ static getInstance(
79
+ logger: IToolLogger,
80
+ fileSystem: IFileSystem,
81
+ ): IWorkflowCompilerPathResolver {
82
+ if (!WorkflowCompilerPathResolver.instance) {
83
+ WorkflowCompilerPathResolver.instance =
84
+ new WorkflowCompilerPathResolver(logger, fileSystem);
85
+ }
86
+ return WorkflowCompilerPathResolver.instance;
87
+ }
88
+
89
+ async getCompilerPathAsync(): Promise<string> {
90
+ if (this.cachedPath) {
91
+ this.logger.info(
92
+ translate.t(
93
+ "toolWorkflowcompiler.pathResolver.info.usingCached",
94
+ { path: this.cachedPath },
95
+ ),
96
+ );
97
+ return this.cachedPath;
98
+ }
99
+
100
+ // Coalesce concurrent callers into a single resolution to avoid
101
+ // parallel dotnet restore processes.
102
+ if (this.pendingResolution) {
103
+ return this.pendingResolution;
104
+ }
105
+
106
+ this.pendingResolution = this.resolveCompilerPath();
107
+ this.cachedPath = await this.pendingResolution;
108
+ this.logger.info(
109
+ translate.t("toolWorkflowcompiler.pathResolver.info.resolved", {
110
+ path: this.cachedPath,
111
+ }),
112
+ );
113
+ return this.cachedPath;
114
+ }
115
+
116
+ private async resolveCompilerPath(): Promise<string> {
117
+ this.logger.info("[PathResolver] resolveCompilerPath started");
118
+
119
+ if (workflowCompilerConfig.workflowCompilerPath) {
120
+ this.logger.info(
121
+ translate.t(
122
+ "toolWorkflowcompiler.pathResolver.info.usingExplicitPath",
123
+ { path: workflowCompilerConfig.workflowCompilerPath },
124
+ ),
125
+ );
126
+ return workflowCompilerConfig.workflowCompilerPath;
127
+ }
128
+
129
+ this.logger.info(
130
+ translate.t("toolWorkflowcompiler.pathResolver.info.resolving"),
131
+ );
132
+
133
+ const envPath = await this.findInEnvLocation();
134
+ if (envPath) return envPath;
135
+
136
+ const version = workflowCompilerConfig.workflowCompilerVersion;
137
+ const compilerPath = this.getNuGetCompilerPath(version);
138
+
139
+ this.logger.info(
140
+ translate.t(
141
+ "toolWorkflowcompiler.pathResolver.info.checkingNuGetCache",
142
+ { path: compilerPath },
143
+ ),
144
+ );
145
+ if (await this.fileSystem.exists(compilerPath)) {
146
+ this.logger.info(
147
+ translate.t(
148
+ "toolWorkflowcompiler.pathResolver.info.foundInNuGetCache",
149
+ ),
150
+ );
151
+ return compilerPath;
152
+ }
153
+
154
+ this.logger.info(
155
+ translate.t(
156
+ "toolWorkflowcompiler.pathResolver.info.notFoundStartingInstall",
157
+ ),
158
+ );
159
+ return this.restoreAndResolve(compilerPath, version);
160
+ }
161
+
162
+ private async findInEnvLocation(): Promise<string | null> {
163
+ const location = process.env.UIPATH_WORKFLOWCOMPILER_LOCATION;
164
+ if (!location) return null;
165
+
166
+ this.logger.info(
167
+ translate.t(
168
+ "toolWorkflowcompiler.pathResolver.info.checkingEnvVar",
169
+ { path: location },
170
+ ),
171
+ );
172
+
173
+ // Env var must point to a full file path (.dll or .exe)
174
+ if (
175
+ (location.endsWith(".dll") || location.endsWith(".exe")) &&
176
+ (await this.fileSystem.exists(location))
177
+ ) {
178
+ this.logger.info(
179
+ translate.t(
180
+ "toolWorkflowcompiler.pathResolver.info.foundViaEnv",
181
+ ),
182
+ );
183
+ return location;
184
+ }
185
+
186
+ return null;
187
+ }
188
+
189
+ private async restoreAndResolve(
190
+ compilerPath: string,
191
+ version: string,
192
+ ): Promise<string> {
193
+ const { packageId } = this.getPlatformInfo();
194
+ const tempDir = await this.tempStorage.getTempFolderPath();
195
+
196
+ try {
197
+ this.logger.info(
198
+ translate.t(
199
+ "toolWorkflowcompiler.pathResolver.info.restoringPackage",
200
+ { packageName: packageId, version, platform: platform() },
201
+ ),
202
+ );
203
+
204
+ const csproj = RESTORE_CSPROJ_TEMPLATE.replace(
205
+ "{packageId}",
206
+ packageId,
207
+ ).replace("{version}", version);
208
+ await this.fileSystem.writeFile(
209
+ Path.join(tempDir, "restore.csproj"),
210
+ csproj,
211
+ );
212
+ await this.fileSystem.writeFile(
213
+ Path.join(tempDir, "nuget.config"),
214
+ EMPTY_NUGET_CONFIG,
215
+ );
216
+
217
+ this.logger.info(
218
+ translate.t(
219
+ "toolWorkflowcompiler.pathResolver.info.runningDotnetRestore",
220
+ ),
221
+ );
222
+ try {
223
+ execFileSync(
224
+ "dotnet",
225
+ [
226
+ "restore",
227
+ "--source",
228
+ NUGET_FEED_URL,
229
+ "--source",
230
+ "https://api.nuget.org/v3/index.json",
231
+ ],
232
+ {
233
+ cwd: tempDir,
234
+ stdio: "pipe",
235
+ },
236
+ );
237
+ } catch (error: unknown) {
238
+ const err = error as Record<string, unknown>;
239
+ const stderr = err?.stderr ? String(err.stderr).trim() : "";
240
+ const stdout = err?.stdout ? String(err.stdout).trim() : "";
241
+ const details =
242
+ stderr ||
243
+ stdout ||
244
+ (error instanceof Error ? error.message : String(error));
245
+ throw new Error(
246
+ translate.t(
247
+ "toolWorkflowcompiler.pathResolver.errors.dotnetRestoreFailed",
248
+ { details },
249
+ ),
250
+ );
251
+ }
252
+ this.logger.info(
253
+ translate.t(
254
+ "toolWorkflowcompiler.pathResolver.info.restoreCompleted",
255
+ ),
256
+ );
257
+
258
+ if (!(await this.fileSystem.exists(compilerPath))) {
259
+ throw new Error(
260
+ translate.t(
261
+ "toolWorkflowcompiler.pathResolver.errors.compilerNotFoundAfterRestore",
262
+ ),
263
+ );
264
+ }
265
+
266
+ this.logger.info(
267
+ translate.t(
268
+ "toolWorkflowcompiler.pathResolver.info.installCompleted",
269
+ ),
270
+ );
271
+ return compilerPath;
272
+ } finally {
273
+ this.logger.info(
274
+ translate.t(
275
+ "toolWorkflowcompiler.pathResolver.info.cleaningUp",
276
+ ),
277
+ );
278
+ await this.tempStorage.cleanup();
279
+ }
280
+ }
281
+
282
+ private getPlatformInfo(): {
283
+ packageId: string;
284
+ compilerFileName: string;
285
+ } {
286
+ const info = PLATFORM_PACKAGES[platform()];
287
+ if (!info) {
288
+ throw new Error(
289
+ translate.t(
290
+ "toolWorkflowcompiler.pathResolver.errors.unsupportedPlatform",
291
+ { platform: platform() },
292
+ ),
293
+ );
294
+ }
295
+ return info;
296
+ }
297
+
298
+ private getNuGetCompilerPath(version: string): string {
299
+ const cacheRoot =
300
+ process.env.NUGET_PACKAGES ??
301
+ Path.join(homedir(), ".nuget", "packages");
302
+ const { packageId, compilerFileName } = this.getPlatformInfo();
303
+ return Path.join(
304
+ cacheRoot,
305
+ packageId.toLowerCase(),
306
+ version.toLowerCase(),
307
+ "WorkflowCompiler",
308
+ compilerFileName,
309
+ );
310
+ }
311
+ }