From 1b21fdbb9b740a62bde781e324415f2b4ec1f8ca Mon Sep 17 00:00:00 2001 From: Joshua Love Date: Fri, 1 May 2026 11:12:11 -0700 Subject: [PATCH] fix(http-client-csharp): make emitter.ts and lib/utils.ts browser-bundleable Moves the Node-only execCSharpGenerator/execAsync helpers (which use child_process) out of lib/utils.ts and into a new lib/exec-utils.ts that is only imported from emit-generate.ts (the Node-only generation entry point). The browser variant (emit-generate.browser.ts) does not import exec-utils, so child_process is no longer pulled into the browser bundle. Also replaces `import { resolve } from "path"` in emitter.ts with the cross-platform `resolvePath` utility from @typespec/compiler so that emitter.ts no longer has a direct dependency on the Node `path` module. This fixes the "Upload playground bundle" pipeline step that was failing with: emitter.js: Could not resolve "path" lib/utils.js: Could not resolve "child_process" Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../emitter/src/emit-generate.ts | 2 +- .../http-client-csharp/emitter/src/emitter.ts | 11 +- .../emitter/src/lib/exec-utils.ts | 138 ++++++++++++++++++ .../emitter/src/lib/utils.ts | 131 +---------------- .../emitter/test/Unit/emitter.test.ts | 4 +- .../emitter/test/Unit/utils.test.ts | 3 +- .../test/Unit/validate-dotnet-sdk.test.ts | 4 +- 7 files changed, 154 insertions(+), 139 deletions(-) create mode 100644 packages/http-client-csharp/emitter/src/lib/exec-utils.ts diff --git a/packages/http-client-csharp/emitter/src/emit-generate.ts b/packages/http-client-csharp/emitter/src/emit-generate.ts index 41d14546b9b..a6421ca8906 100644 --- a/packages/http-client-csharp/emitter/src/emit-generate.ts +++ b/packages/http-client-csharp/emitter/src/emit-generate.ts @@ -19,8 +19,8 @@ import { configurationFileName, tspOutputFileName, } from "./constants.js"; +import { execAsync, execCSharpGenerator } from "./lib/exec-utils.js"; import { createDiagnostic } from "./lib/lib.js"; -import { execAsync, execCSharpGenerator } from "./lib/utils.js"; import { CSharpEmitterContext } from "./sdk-context.js"; export interface GenerateOptions { diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index a24e86119e7..c49591c53d2 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -2,8 +2,13 @@ // Licensed under the MIT License. See License.txt in the project root for license information. import { createSdkContext, SdkContext } from "@azure-tools/typespec-client-generator-core"; -import { createDiagnosticCollector, Diagnostic, EmitContext, Program } from "@typespec/compiler"; -import { resolve } from "path"; +import { + createDiagnosticCollector, + Diagnostic, + EmitContext, + Program, + resolvePath, +} from "@typespec/compiler"; import { serializeCodeModel } from "./code-model-writer.js"; import { generate } from "./emit-generate.js"; import { createModel } from "./lib/client-model-builder.js"; @@ -49,7 +54,7 @@ export async function emitCodeModel( // Resolve plugin paths to absolute if specified if (options["plugins"]) { - options["plugins"] = options["plugins"].map((p) => resolve(outputFolder, p)); + options["plugins"] = options["plugins"].map((p) => resolvePath(outputFolder, p)); } /* set the log level. */ diff --git a/packages/http-client-csharp/emitter/src/lib/exec-utils.ts b/packages/http-client-csharp/emitter/src/lib/exec-utils.ts new file mode 100644 index 00000000000..8e9684718da --- /dev/null +++ b/packages/http-client-csharp/emitter/src/lib/exec-utils.ts @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +// Node.js-only helpers that wrap `child_process`. These are kept in a separate +// file so that browser bundles (which do not support `child_process`) do not +// pull them in transitively via `lib/utils.ts`. + +import { NoTarget, Type } from "@typespec/compiler"; +import { spawn, SpawnOptions } from "child_process"; +import { CSharpEmitterContext } from "../sdk-context.js"; + +export async function execCSharpGenerator( + context: CSharpEmitterContext, + options: { + generatorPath: string; + outputFolder: string; + generatorName: string; + newProject: boolean; + debug: boolean; + }, +): Promise<{ exitCode: number; stderr: string; proc: any }> { + const command = "dotnet"; + const args = [ + "--roll-forward", + "Major", + options.generatorPath, + options.outputFolder, + "-g", + options.generatorName, + ]; + if (options.newProject) { + args.push("--new-project"); + } + if (options.debug) { + args.push("--debug"); + } + context.logger.info(`${command} ${args.join(" ")}`); + + const child = spawn(command, args, { stdio: "pipe" }); + + const stderr: Buffer[] = []; + return new Promise((resolve, reject) => { + let buffer = ""; + + child.stdout?.on("data", (data) => { + buffer += data.toString(); + let index; + while ((index = buffer.indexOf("\n")) !== -1) { + const message = buffer.slice(0, index); + buffer = buffer.slice(index + 1); + processJsonRpc(context, message); + } + }); + + child.stderr?.on("data", (data) => { + stderr.push(data); + }); + + child.on("error", (error) => { + reject(error); + }); + + child.on("exit", (exitCode) => { + resolve({ + exitCode: exitCode ?? -1, + stderr: Buffer.concat(stderr).toString(), + proc: child, + }); + }); + }); +} + +function processJsonRpc(context: CSharpEmitterContext, message: string) { + const response = JSON.parse(message); + const method = response.method; + const params = response.params; + switch (method) { + case "trace": + context.logger.trace(params.level, params.message); + break; + case "diagnostic": + let crossLanguageDefinitionId: string | undefined; + if ("crossLanguageDefinitionId" in params) { + crossLanguageDefinitionId = params.crossLanguageDefinitionId; + } + // Use program.reportDiagnostic for diagnostics from C# so that we don't + // have to duplicate the codes in the emitter. + context.program.reportDiagnostic({ + code: params.code, + message: params.message, + severity: params.severity, + target: findTarget(crossLanguageDefinitionId) ?? NoTarget, + }); + break; + } + + function findTarget(crossLanguageDefinitionId: string | undefined): Type | undefined { + if (crossLanguageDefinitionId === undefined) { + return undefined; + } + return context.__typeCache.crossLanguageDefinitionIds.get(crossLanguageDefinitionId); + } +} + +export async function execAsync( + command: string, + args: string[] = [], + options: SpawnOptions = {}, +): Promise<{ exitCode: number; stdio: string; stdout: string; stderr: string; proc: any }> { + const child = spawn(command, args, options); + + return new Promise((resolve, reject) => { + child.on("error", (error) => { + reject(error); + }); + const stdio: Buffer[] = []; + const stdout: Buffer[] = []; + const stderr: Buffer[] = []; + child.stdout?.on("data", (data) => { + stdout.push(data); + stdio.push(data); + }); + child.stderr?.on("data", (data) => { + stderr.push(data); + stdio.push(data); + }); + + child.on("exit", (exitCode) => { + resolve({ + exitCode: exitCode ?? -1, + stdio: Buffer.concat(stdio).toString(), + stdout: Buffer.concat(stdout).toString(), + stderr: Buffer.concat(stderr).toString(), + proc: child, + }); + }); + }); +} diff --git a/packages/http-client-csharp/emitter/src/lib/utils.ts b/packages/http-client-csharp/emitter/src/lib/utils.ts index ed535d200f2..f2831b40ee4 100644 --- a/packages/http-client-csharp/emitter/src/lib/utils.ts +++ b/packages/http-client-csharp/emitter/src/lib/utils.ts @@ -7,139 +7,10 @@ import { SdkModelPropertyType, isReadOnly as tcgcIsReadOnly, } from "@azure-tools/typespec-client-generator-core"; -import { getNamespaceFullName, Namespace, NoTarget, Type } from "@typespec/compiler"; +import { getNamespaceFullName, Namespace } from "@typespec/compiler"; import { Visibility } from "@typespec/http"; -import { spawn, SpawnOptions } from "child_process"; import { CSharpEmitterContext } from "../sdk-context.js"; -export async function execCSharpGenerator( - context: CSharpEmitterContext, - options: { - generatorPath: string; - outputFolder: string; - generatorName: string; - newProject: boolean; - debug: boolean; - }, -): Promise<{ exitCode: number; stderr: string; proc: any }> { - const command = "dotnet"; - const args = [ - "--roll-forward", - "Major", - options.generatorPath, - options.outputFolder, - "-g", - options.generatorName, - ]; - if (options.newProject) { - args.push("--new-project"); - } - if (options.debug) { - args.push("--debug"); - } - context.logger.info(`${command} ${args.join(" ")}`); - - const child = spawn(command, args, { stdio: "pipe" }); - - const stderr: Buffer[] = []; - return new Promise((resolve, reject) => { - let buffer = ""; - - child.stdout?.on("data", (data) => { - buffer += data.toString(); - let index; - while ((index = buffer.indexOf("\n")) !== -1) { - const message = buffer.slice(0, index); - buffer = buffer.slice(index + 1); - processJsonRpc(context, message); - } - }); - - child.stderr?.on("data", (data) => { - stderr.push(data); - }); - - child.on("error", (error) => { - reject(error); - }); - - child.on("exit", (exitCode) => { - resolve({ - exitCode: exitCode ?? -1, - stderr: Buffer.concat(stderr).toString(), - proc: child, - }); - }); - }); -} - -function processJsonRpc(context: CSharpEmitterContext, message: string) { - const response = JSON.parse(message); - const method = response.method; - const params = response.params; - switch (method) { - case "trace": - context.logger.trace(params.level, params.message); - break; - case "diagnostic": - let crossLanguageDefinitionId: string | undefined; - if ("crossLanguageDefinitionId" in params) { - crossLanguageDefinitionId = params.crossLanguageDefinitionId; - } - // Use program.reportDiagnostic for diagnostics from C# so that we don't - // have to duplicate the codes in the emitter. - context.program.reportDiagnostic({ - code: params.code, - message: params.message, - severity: params.severity, - target: findTarget(crossLanguageDefinitionId) ?? NoTarget, - }); - break; - } - - function findTarget(crossLanguageDefinitionId: string | undefined): Type | undefined { - if (crossLanguageDefinitionId === undefined) { - return undefined; - } - return context.__typeCache.crossLanguageDefinitionIds.get(crossLanguageDefinitionId); - } -} - -export async function execAsync( - command: string, - args: string[] = [], - options: SpawnOptions = {}, -): Promise<{ exitCode: number; stdio: string; stdout: string; stderr: string; proc: any }> { - const child = spawn(command, args, options); - - return new Promise((resolve, reject) => { - child.on("error", (error) => { - reject(error); - }); - const stdio: Buffer[] = []; - const stdout: Buffer[] = []; - const stderr: Buffer[] = []; - child.stdout?.on("data", (data) => { - stdout.push(data); - stdio.push(data); - }); - child.stderr?.on("data", (data) => { - stderr.push(data); - stdio.push(data); - }); - - child.on("exit", (exitCode) => { - resolve({ - exitCode: exitCode ?? -1, - stdio: Buffer.concat(stdio).toString(), - stdout: Buffer.concat(stdout).toString(), - stderr: Buffer.concat(stderr).toString(), - proc: child, - }); - }); - }); -} - export function getClientNamespaceString(context: CSharpEmitterContext): string | undefined { const packageName = context.emitContext.options["package-name"]; const serviceNamespaces = listAllServiceNamespaces(context); diff --git a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts index c937a9ea0d4..2f5839eb997 100644 --- a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts @@ -4,7 +4,7 @@ import { EmitContext, Program } from "@typespec/compiler"; import { TestHost } from "@typespec/compiler/testing"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { generate } from "../../src/emit-generate.js"; -import { execAsync, execCSharpGenerator } from "../../src/lib/utils.js"; +import { execAsync, execCSharpGenerator } from "../../src/lib/exec-utils.js"; import { CSharpEmitterOptions } from "../../src/options.js"; import { CodeModel } from "../../src/type/code-model.js"; import { @@ -54,7 +54,7 @@ describe("$onEmit tests", () => { }), })); - vi.mock("../../src/lib/utils.js", () => ({ + vi.mock("../../src/lib/exec-utils.js", () => ({ execCSharpGenerator: vi.fn(), execAsync: vi.fn(), })); diff --git a/packages/http-client-csharp/emitter/test/Unit/utils.test.ts b/packages/http-client-csharp/emitter/test/Unit/utils.test.ts index c5a7b94d6fa..f5d42ff505f 100644 --- a/packages/http-client-csharp/emitter/test/Unit/utils.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/utils.test.ts @@ -2,7 +2,8 @@ import { listAllServiceNamespaces } from "@azure-tools/typespec-client-generator import * as childProcess from "child_process"; import { EventEmitter } from "events"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { execCSharpGenerator, getClientNamespaceStringHelper } from "../../src/lib/utils.js"; +import { execCSharpGenerator } from "../../src/lib/exec-utils.js"; +import { getClientNamespaceStringHelper } from "../../src/lib/utils.js"; import { CSharpEmitterContext } from "../../src/sdk-context.js"; import { createCSharpSdkContext, diff --git a/packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts b/packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts index a3f0eb397fd..37d801200b1 100644 --- a/packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/validate-dotnet-sdk.test.ts @@ -4,7 +4,7 @@ import { Diagnostic, Program } from "@typespec/compiler"; import { TestHost } from "@typespec/compiler/testing"; import { strictEqual } from "assert"; import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; -import { execAsync } from "../../src/lib/utils.js"; +import { execAsync } from "../../src/lib/exec-utils.js"; import { createCSharpSdkContext, createEmitterContext, @@ -33,7 +33,7 @@ describe("Test _validateDotNetSdk", () => { ); // Restore all mocks before each test vi.restoreAllMocks(); - vi.mock("../../src/lib/utils.js", () => ({ + vi.mock("../../src/lib/exec-utils.js", () => ({ execCSharpGenerator: vi.fn(), execAsync: vi.fn(), }));