From 6ea7378455f7285fac4da63155961cefef1ed6f2 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 2 Apr 2026 17:38:40 -0400 Subject: [PATCH 1/3] C# playground wasm POC --- .../http-client-csharp/emitter/src/emitter.ts | 205 ++++-------------- .../emitter/src/lib/dotnet-exec.ts | 134 ++++++++++++ .../emitter/src/lib/dotnet-host.ts | 203 +++++++++++++++++ .../emitter/src/lib/utils.ts | 131 +---------- .../emitter/src/lib/wasm-generator.ts | 94 ++++++++ .../http-client-csharp/emitter/src/options.ts | 9 + .../emitter/test/Unit/emitter.test.ts | 100 +++------ .../emitter/test/Unit/utils.test.ts | 3 +- .../Microsoft.TypeSpec.Generator.Wasm.csproj | 30 +++ .../src/Program.cs | 8 + .../src/WasmGenerator.cs | 109 ++++++++++ .../src/main.js | 1 + .../src/Properties/AssemblyInfo.cs | 1 + packages/http-client-csharp/global.json | 2 +- packages/playground-website/package.json | 3 + packages/playground-website/src/config.ts | 3 + packages/playground-website/vite.config.ts | 40 ++++ pnpm-lock.yaml | 77 +++++++ 18 files changed, 793 insertions(+), 360 deletions(-) create mode 100644 packages/http-client-csharp/emitter/src/lib/dotnet-exec.ts create mode 100644 packages/http-client-csharp/emitter/src/lib/dotnet-host.ts create mode 100644 packages/http-client-csharp/emitter/src/lib/wasm-generator.ts create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Microsoft.TypeSpec.Generator.Wasm.csproj create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Program.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/main.js diff --git a/packages/http-client-csharp/emitter/src/emitter.ts b/packages/http-client-csharp/emitter/src/emitter.ts index 936f79dd191..83e6d65cd5f 100644 --- a/packages/http-client-csharp/emitter/src/emitter.ts +++ b/packages/http-client-csharp/emitter/src/emitter.ts @@ -6,51 +6,20 @@ import { createDiagnosticCollector, Diagnostic, EmitContext, - getDirectoryPath, - joinPaths, - NoTarget, Program, resolvePath, } from "@typespec/compiler"; -import fs, { statSync } from "fs"; -import { dirname } from "path"; -import { fileURLToPath } from "url"; import { writeCodeModel, writeConfiguration } from "./code-model-writer.js"; -import { - _minSupportedDotNetSdkVersion, - configurationFileName, - tspOutputFileName, -} from "./constants.js"; +import { configurationFileName, tspOutputFileName } from "./constants.js"; import { createModel } from "./lib/client-model-builder.js"; -import { createDiagnostic } from "./lib/lib.js"; import { LoggerLevel } from "./lib/logger-level.js"; import { Logger } from "./lib/logger.js"; -import { execAsync, execCSharpGenerator } from "./lib/utils.js"; +import { generateCSharpFromWasm } from "./lib/wasm-generator.js"; import { CSharpEmitterOptions, resolveOptions } from "./options.js"; import { createCSharpEmitterContext, CSharpEmitterContext } from "./sdk-context.js"; import { CodeModel } from "./type/code-model.js"; import { Configuration } from "./type/configuration.js"; -/** - * Look for the project root by looking up until a `package.json` is found. - * @param path Path to start looking - */ -function findProjectRoot(path: string): string | undefined { - let current = path; - while (true) { - const pkgPath = joinPaths(current, "package.json"); - const stats = checkFile(pkgPath); - if (stats?.isFile()) { - return current; - } - const parent = getDirectoryPath(current); - if (parent === current) { - return undefined; - } - current = parent; - } -} - /** * Creates a code model by executing the full emission logic. * This function can be called by downstream emitters to generate a code model and collect diagnostics. @@ -107,12 +76,6 @@ export async function emitCodeModel( // Apply optional code model update callback const updatedRoot = updateCodeModel ? updateCodeModel(root, sdkContext) : root; - const generatedFolder = resolvePath(outputFolder, "src", "Generated"); - - if (!fs.existsSync(generatedFolder)) { - fs.mkdirSync(generatedFolder, { recursive: true }); - } - // emit tspCodeModel.json await writeCodeModel(sdkContext, updatedRoot, outputFolder); @@ -122,50 +85,52 @@ export async function emitCodeModel( //emit configuration.json await writeConfiguration(sdkContext, configurations, outputFolder); - const csProjFile = resolvePath( - outputFolder, - "src", - `${configurations["package-name"]}.csproj`, - ); - logger.info(`Checking if ${csProjFile} exists`); - - const emitterPath = options["emitter-extension-path"] ?? import.meta.url; - const projectRoot = findProjectRoot(dirname(fileURLToPath(emitterPath))); - const generatorPath = resolvePath( - projectRoot + "/dist/generator/Microsoft.TypeSpec.Generator.dll", - ); - - try { - const result = await execCSharpGenerator(sdkContext, { - generatorPath: generatorPath, - outputFolder: outputFolder, - generatorName: options["generator-name"], - newProject: options["new-project"] || !checkFile(csProjFile), - debug: options.debug ?? false, + // Determine execution mode based on environment + const isBrowser = + typeof globalThis.process === "undefined" || + typeof globalThis.process?.versions?.node === "undefined"; + const shouldSkipGenerator = options["skip-generator"] ?? false; + + if (!isBrowser && !shouldSkipGenerator) { + // Node.js: run the dotnet C# generator as a child process + const { runDotnetGenerator } = await import("./lib/dotnet-host.js"); + await runDotnetGenerator(sdkContext, diagnostics, { + resolvedOptions: options, + configurations, + outputFolder, + context, + logger, }); - if (result.exitCode !== 0) { - const isValid = diagnostics.pipe( - await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), + } else if (isBrowser && !shouldSkipGenerator) { + // Browser: use the WASM generator to produce C# files + console.log("[http-client-csharp] Browser detected, attempting WASM generator..."); + try { + // Read the code model and config that were already written to the virtual FS + const codeModelFile = await context.program.host.readFile( + resolvePath(outputFolder, tspOutputFileName), + ); + const configFile = await context.program.host.readFile( + resolvePath(outputFolder, configurationFileName), ); - // if the dotnet sdk is valid, the error is not dependency issue, log it as normal - if (isValid) { - throw new Error( - `Failed to generate the library. Exit code: ${result.exitCode}.\nStackTrace: \n${result.stderr}`, + const generatedFiles = await generateCSharpFromWasm( + codeModelFile.text, + configFile.text, + ); + + // Write generated C# files to the virtual filesystem + for (const [filePath, content] of Object.entries(generatedFiles)) { + await context.program.host.writeFile( + resolvePath(outputFolder, filePath), + content, ); } + } catch (wasmError: any) { + // Log to console for debugging and report as diagnostic + console.error("[http-client-csharp] WASM generator error:", wasmError); + logger.info(`WASM generator error: ${wasmError.message ?? wasmError}`); } - } catch (error: any) { - const isValid = diagnostics.pipe( - await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), - ); - // if the dotnet sdk is valid, the error is not dependency issue, log it as normal - if (isValid) throw new Error(error, { cause: error }); - } - if (!options["save-inputs"]) { - // delete - context.program.host.rm(resolvePath(outputFolder, tspOutputFileName)); - context.program.host.rm(resolvePath(outputFolder, configurationFileName)); } + // If skip-generator is true, we already wrote the code model files above } } @@ -191,6 +156,7 @@ export function createConfiguration( "new-project", "sdk-context-options", "save-inputs", + "skip-generator", "generator-name", "debug", "logLevel", @@ -213,88 +179,3 @@ export function createConfiguration( license: sdkContext.sdkPackage.licenseInfo, }; } - -/** check the dotnet sdk installation. - * Report diagnostic if dotnet sdk is not installed or its version does not meet prerequisite - * @param sdkContext - The SDK context - * @param minVersionRequisite - The minimum required major version - * @returns A tuple containing whether the SDK is valid and any diagnostics - * @internal - */ -export async function _validateDotNetSdk( - sdkContext: CSharpEmitterContext, - minMajorVersion: number, -): Promise<[boolean, readonly Diagnostic[]]> { - const diagnostics = createDiagnosticCollector(); - try { - const result = await execAsync("dotnet", ["--version"], { stdio: "pipe" }); - return diagnostics.wrap( - diagnostics.pipe(validateDotNetSdkVersionCore(sdkContext, result.stdout, minMajorVersion)), - ); - } catch (error: any) { - if (error && "code" in error && error["code"] === "ENOENT") { - diagnostics.add( - createDiagnostic({ - code: "invalid-dotnet-sdk-dependency", - messageId: "missing", - format: { - dotnetMajorVersion: `${minMajorVersion}`, - downloadUrl: "https://dotnet.microsoft.com/", - }, - target: NoTarget, - }), - ); - } - return diagnostics.wrap(false); - } -} - -function validateDotNetSdkVersionCore( - sdkContext: CSharpEmitterContext, - version: string, - minMajorVersion: number, -): [boolean, readonly Diagnostic[]] { - const diagnostics = createDiagnosticCollector(); - if (version) { - const dotIndex = version.indexOf("."); - const firstPart = dotIndex === -1 ? version : version.substring(0, dotIndex); - const major = Number(firstPart); - - if (isNaN(major)) { - return diagnostics.wrap(false); - } - if (major < minMajorVersion) { - diagnostics.add( - createDiagnostic({ - code: "invalid-dotnet-sdk-dependency", - messageId: "invalidVersion", - format: { - installedVersion: version, - dotnetMajorVersion: `${minMajorVersion}`, - downloadUrl: "https://dotnet.microsoft.com/", - }, - target: NoTarget, - }), - ); - return diagnostics.wrap(false); - } - return diagnostics.wrap(true); - } else { - diagnostics.add( - createDiagnostic({ - code: "general-error", - format: { message: "Cannot get the installed .NET SDK version." }, - target: NoTarget, - }), - ); - return diagnostics.wrap(false); - } -} - -function checkFile(pkgPath: string) { - try { - return statSync(pkgPath); - } catch (error) { - return undefined; - } -} diff --git a/packages/http-client-csharp/emitter/src/lib/dotnet-exec.ts b/packages/http-client-csharp/emitter/src/lib/dotnet-exec.ts new file mode 100644 index 00000000000..45b561569fd --- /dev/null +++ b/packages/http-client-csharp/emitter/src/lib/dotnet-exec.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +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/dotnet-host.ts b/packages/http-client-csharp/emitter/src/lib/dotnet-host.ts new file mode 100644 index 00000000000..fb9d134e756 --- /dev/null +++ b/packages/http-client-csharp/emitter/src/lib/dotnet-host.ts @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +import { + createDiagnosticCollector, + Diagnostic, + DiagnosticCollector, + EmitContext, + getDirectoryPath, + joinPaths, + NoTarget, + resolvePath, +} from "@typespec/compiler"; +import fs, { statSync } from "fs"; +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { + _minSupportedDotNetSdkVersion, + configurationFileName, + tspOutputFileName, +} from "../constants.js"; +import { execAsync, execCSharpGenerator } from "./dotnet-exec.js"; +import { createDiagnostic } from "./lib.js"; +import { Logger } from "./logger.js"; +import { CSharpEmitterOptions } from "../options.js"; +import { CSharpEmitterContext } from "../sdk-context.js"; +import { Configuration } from "../type/configuration.js"; + +/** + * Look for the project root by looking up until a `package.json` is found. + * @param path Path to start looking + */ +function findProjectRoot(path: string): string | undefined { + let current = path; + while (true) { + const pkgPath = joinPaths(current, "package.json"); + const stats = checkFile(pkgPath); + if (stats?.isFile()) { + return current; + } + const parent = getDirectoryPath(current); + if (parent === current) { + return undefined; + } + current = parent; + } +} + +function checkFile(pkgPath: string) { + try { + return statSync(pkgPath); + } catch (error) { + return undefined; + } +} + +/** check the dotnet sdk installation. + * Report diagnostic if dotnet sdk is not installed or its version does not meet prerequisite + * @param sdkContext - The SDK context + * @param minVersionRequisite - The minimum required major version + * @returns A tuple containing whether the SDK is valid and any diagnostics + * @internal + */ +export async function _validateDotNetSdk( + sdkContext: CSharpEmitterContext, + minMajorVersion: number, +): Promise<[boolean, readonly Diagnostic[]]> { + const diagnostics = createDiagnosticCollector(); + try { + const result = await execAsync("dotnet", ["--version"], { stdio: "pipe" }); + return diagnostics.wrap( + diagnostics.pipe(validateDotNetSdkVersionCore(sdkContext, result.stdout, minMajorVersion)), + ); + } catch (error: any) { + if (error && "code" in error && error["code"] === "ENOENT") { + diagnostics.add( + createDiagnostic({ + code: "invalid-dotnet-sdk-dependency", + messageId: "missing", + format: { + dotnetMajorVersion: `${minMajorVersion}`, + downloadUrl: "https://dotnet.microsoft.com/", + }, + target: NoTarget, + }), + ); + } + return diagnostics.wrap(false); + } +} + +function validateDotNetSdkVersionCore( + sdkContext: CSharpEmitterContext, + version: string, + minMajorVersion: number, +): [boolean, readonly Diagnostic[]] { + const diagnostics = createDiagnosticCollector(); + if (version) { + const dotIndex = version.indexOf("."); + const firstPart = dotIndex === -1 ? version : version.substring(0, dotIndex); + const major = Number(firstPart); + + if (isNaN(major)) { + return diagnostics.wrap(false); + } + if (major < minMajorVersion) { + diagnostics.add( + createDiagnostic({ + code: "invalid-dotnet-sdk-dependency", + messageId: "invalidVersion", + format: { + installedVersion: version, + dotnetMajorVersion: `${minMajorVersion}`, + downloadUrl: "https://dotnet.microsoft.com/", + }, + target: NoTarget, + }), + ); + return diagnostics.wrap(false); + } + return diagnostics.wrap(true); + } else { + diagnostics.add( + createDiagnostic({ + code: "general-error", + format: { message: "Cannot get the installed .NET SDK version." }, + target: NoTarget, + }), + ); + return diagnostics.wrap(false); + } +} + +/** + * Runs the dotnet C# generator. This function encapsulates all Node.js-dependent code + * (filesystem operations, child process spawning) needed for the generator execution. + * It is loaded via dynamic import so that browser bundles never pull in Node.js modules. + * @internal + */ +export async function runDotnetGenerator( + sdkContext: CSharpEmitterContext, + diagnostics: DiagnosticCollector, + params: { + resolvedOptions: CSharpEmitterOptions & { "generator-name": string }; + configurations: Configuration; + outputFolder: string; + context: EmitContext; + logger: Logger; + }, +): Promise { + const { resolvedOptions: options, configurations, outputFolder, context, logger } = params; + + const generatedFolder = resolvePath(outputFolder, "src", "Generated"); + + if (!fs.existsSync(generatedFolder)) { + fs.mkdirSync(generatedFolder, { recursive: true }); + } + + const csProjFile = resolvePath( + outputFolder, + "src", + `${configurations["package-name"]}.csproj`, + ); + logger.info(`Checking if ${csProjFile} exists`); + + const emitterPath = options["emitter-extension-path"] ?? import.meta.url; + const projectRoot = findProjectRoot(dirname(fileURLToPath(emitterPath))); + const generatorPath = resolvePath( + projectRoot + "/dist/generator/Microsoft.TypeSpec.Generator.dll", + ); + + try { + const result = await execCSharpGenerator(sdkContext, { + generatorPath: generatorPath, + outputFolder: outputFolder, + generatorName: options["generator-name"], + newProject: options["new-project"] || !checkFile(csProjFile), + debug: options.debug ?? false, + }); + if (result.exitCode !== 0) { + const isValid = diagnostics.pipe( + await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), + ); + // if the dotnet sdk is valid, the error is not dependency issue, log it as normal + if (isValid) { + throw new Error( + `Failed to generate the library. Exit code: ${result.exitCode}.\nStackTrace: \n${result.stderr}`, + ); + } + } + } catch (error: any) { + const isValid = diagnostics.pipe( + await _validateDotNetSdk(sdkContext, _minSupportedDotNetSdkVersion), + ); + // if the dotnet sdk is valid, the error is not dependency issue, log it as normal + if (isValid) throw new Error(error, { cause: error }); + } + if (!options["save-inputs"]) { + // delete + context.program.host.rm(resolvePath(outputFolder, tspOutputFileName)); + context.program.host.rm(resolvePath(outputFolder, configurationFileName)); + } +} diff --git a/packages/http-client-csharp/emitter/src/lib/utils.ts b/packages/http-client-csharp/emitter/src/lib/utils.ts index aa8af4d3889..1cd505e272c 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/src/lib/wasm-generator.ts b/packages/http-client-csharp/emitter/src/lib/wasm-generator.ts new file mode 100644 index 00000000000..ca80ea45778 --- /dev/null +++ b/packages/http-client-csharp/emitter/src/lib/wasm-generator.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +/** + * Bridge between the TypeSpec JS emitter and the C# WASM generator. + * This module lazily loads the .NET WASM runtime and calls the C# generator + * to produce generated C# files from the tspCodeModel.json intermediate representation. + */ + +let wasmExports: any = null; +let initPromise: Promise | null = null; + +/** + * Get the base URL for the WASM bundle assets. + * In the playground, these are served from a configurable path. + */ +function getWasmBaseUrl(): string { + // Look for a global configuration, fall back to a default path + return (globalThis as any).__TYPESPEC_CSHARP_WASM_BASE_URL ?? "/wasm/csharp/"; +} + +async function initializeWasm(): Promise { + if (wasmExports) return; + + const baseUrl = getWasmBaseUrl(); + const dotnetUrl = `${baseUrl}_framework/dotnet.js`; + + // Dynamically import the .NET WASM runtime + const { dotnet } = await import(/* @vite-ignore */ dotnetUrl); + + // Configure the runtime to load assets from the correct base URL + const { getAssemblyExports, getConfig } = await dotnet + .withModuleConfig({ + locateFile: (path: string) => `${baseUrl}_framework/${path}`, + }) + .withResourceLoader((_type: string, name: string, _defaultUri: string) => { + return `${baseUrl}_framework/${name}`; + }) + .create(); + + const config = getConfig(); + wasmExports = await getAssemblyExports(config.mainAssemblyName); +} + +/** + * Generate C# source files from code model JSON using the WASM generator. + * + * @param codeModelJson - The tspCodeModel.json content + * @param configurationJson - The Configuration.json content + * @returns A map of file paths to generated C# content + */ +export async function generateCSharpFromWasm( + codeModelJson: string, + configurationJson: string, +): Promise> { + // Lazy-initialize the WASM runtime + if (!initPromise) { + initPromise = initializeWasm(); + } + await initPromise; + + if (!wasmExports) { + throw new Error("Failed to initialize the C# WASM generator"); + } + + // Call the [JSExport] method + const resultJson = + wasmExports.Microsoft.TypeSpec.Generator.Wasm.WasmGenerator.Generate( + codeModelJson, + configurationJson, + ); + + const result: Record = JSON.parse(resultJson); + + // Check for errors from the C# side + if (result["__error"]) { + throw new Error(`C# generator error: ${result["__error"]}`); + } + + return result; +} + +/** + * Check if the WASM generator is available (i.e., the assets are served). + */ +export async function isWasmGeneratorAvailable(): Promise { + try { + const baseUrl = getWasmBaseUrl(); + const response = await fetch(`${baseUrl}_framework/dotnet.js`, { method: "HEAD" }); + return response.ok; + } catch { + return false; + } +} diff --git a/packages/http-client-csharp/emitter/src/options.ts b/packages/http-client-csharp/emitter/src/options.ts index 9ae08884a9e..78888d029ef 100644 --- a/packages/http-client-csharp/emitter/src/options.ts +++ b/packages/http-client-csharp/emitter/src/options.ts @@ -13,6 +13,7 @@ export interface CSharpEmitterOptions { "unreferenced-types-handling"?: "removeOrInternalize" | "internalize" | "keepAll"; "new-project"?: boolean; "save-inputs"?: boolean; + "skip-generator"?: boolean; debug?: boolean; logLevel?: LoggerLevel; "disable-xml-docs"?: boolean; @@ -77,6 +78,13 @@ export const CSharpEmitterOptionsSchema: JSONSchemaType = description: "Set to `true` to save the `tspCodeModel.json` and `Configuration.json` files that are emitted and used as inputs to the generator. The default value is `false`.", }, + "skip-generator": { + type: "boolean", + nullable: true, + description: + "Set to `true` to skip running the C# generator (dotnet) and only emit the code model JSON files. " + + "This is useful for browser environments where dotnet is not available. The default value is `false`.", + }, "package-name": { type: "string", nullable: true, @@ -145,6 +153,7 @@ export const defaultOptions = { "api-version": "latest", "new-project": false, "save-inputs": false, + "skip-generator": undefined, "generate-protocol-methods": true, "generate-convenience-methods": true, "package-name": undefined, 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 d3078c9dc59..007b77b07d8 100644 --- a/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts +++ b/packages/http-client-csharp/emitter/test/Unit/emitter.test.ts @@ -3,9 +3,8 @@ vi.resetModules(); import { Diagnostic, EmitContext, Program } from "@typespec/compiler"; import { TestHost } from "@typespec/compiler/testing"; import { strictEqual } from "assert"; -import { statSync } from "fs"; import { beforeEach, describe, expect, it, Mock, vi } from "vitest"; -import { execAsync, execCSharpGenerator } from "../../src/lib/utils.js"; +import { execAsync } from "../../src/lib/dotnet-exec.js"; import { CSharpEmitterOptions } from "../../src/options.js"; import { CodeModel } from "../../src/type/code-model.js"; import { @@ -16,6 +15,10 @@ import { typeSpecCompile, } from "./utils/test-util.js"; +const mocks = vi.hoisted(() => ({ + runDotnetGenerator: vi.fn().mockResolvedValue(undefined), +})); + describe("$onEmit tests", () => { let program: Program; let $onEmit: (arg0: EmitContext) => any; @@ -23,17 +26,11 @@ describe("$onEmit tests", () => { context: EmitContext, updateCodeModel?: (model: CodeModel, context: any) => CodeModel, ) => any; + let runDotnetGenerator: Mock; beforeEach(async () => { // Reset the dynamically imported module to ensure a clean state vi.resetModules(); vi.clearAllMocks(); - vi.mock("fs", async (importOriginal) => { - const actualFs = await importOriginal(); - return { - ...actualFs, - statSync: vi.fn(), - }; - }); vi.mock("@typespec/compiler", async (importOriginal) => { const actual = await importOriginal(); @@ -56,10 +53,15 @@ describe("$onEmit tests", () => { }), })); - vi.mock("../../src/lib/utils.js", () => ({ - execCSharpGenerator: vi.fn(), - execAsync: vi.fn(), - })); + vi.mock("../../src/lib/dotnet-host.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + runDotnetGenerator: mocks.runDotnetGenerator, + }; + }); + + runDotnetGenerator = mocks.runDotnetGenerator; vi.mock("../../src/lib/client-model-builder.js", () => ({ createModel: vi.fn().mockReturnValue([{ name: "TestNamespace" }, []]), @@ -119,68 +121,34 @@ describe("$onEmit tests", () => { ); }); - it("should set newProject to TRUE if .csproj file DOES NOT exist", async () => { - vi.mocked(statSync).mockImplementation(() => { - throw new Error("File not found"); - }); - + it("should call runDotnetGenerator when skip-generator is false (default)", async () => { const context: EmitContext = createEmitterContext(program); await $onEmit(context); - - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: true, // Ensure this is passed as true - debug: false, - }); + expect(runDotnetGenerator).toHaveBeenCalledTimes(1); }); - it("should set newProject to FALSE if .csproj file DOES exist", async () => { - vi.mocked(statSync).mockReturnValue({ isFile: () => true } as any); - - const context: EmitContext = createEmitterContext(program); - await $onEmit(context); - - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: false, // Ensure this is passed as false - debug: false, - }); - }); - - it("should set newProject to TRUE if passed in options", async () => { - vi.mocked(statSync).mockReturnValue({ isFile: () => true } as any); - + it("should NOT call runDotnetGenerator when skip-generator is true", async () => { const context: EmitContext = createEmitterContext(program, { - "new-project": true, + "skip-generator": true, }); await $onEmit(context); - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: true, // Ensure this is passed as true - debug: false, - }); + expect(runDotnetGenerator).not.toHaveBeenCalled(); }); - it("should set newProject to FALSE if passed in options", async () => { - vi.mocked(statSync).mockReturnValue({ isFile: () => true } as any); - + it("should pass new-project option to runDotnetGenerator", async () => { const context: EmitContext = createEmitterContext(program, { - "new-project": false, + "new-project": true, }); await $onEmit(context); - expect(execCSharpGenerator).toHaveBeenCalledWith(expect.anything(), { - generatorPath: expect.any(String), - outputFolder: undefined, - generatorName: "ScmCodeModelGenerator", - newProject: false, // Ensure this is passed as true - debug: false, - }); + expect(runDotnetGenerator).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + resolvedOptions: expect.objectContaining({ + "new-project": true, + }), + }), + ); }); }); @@ -258,14 +226,14 @@ describe("Test _validateDotNetSdk", () => { ); // Restore all mocks before each test vi.restoreAllMocks(); - vi.mock("../../src/lib/utils.js", () => ({ + vi.mock("../../src/lib/dotnet-exec.js", () => ({ execCSharpGenerator: vi.fn(), execAsync: vi.fn(), })); - // dynamically import the module to get the $onEmit function + // dynamically import the module to get the _validateDotNetSdk function // we avoid importing it at the top to allow mocking of dependencies - _validateDotNetSdk = (await import("../../src/emitter.js"))._validateDotNetSdk; + _validateDotNetSdk = (await import("../../src/lib/dotnet-host.js"))._validateDotNetSdk; }); it("should return false and report diagnostic when dotnet SDK is not installed.", async () => { 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..7ac046b736c 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/dotnet-exec.js"; +import { getClientNamespaceStringHelper } from "../../src/lib/utils.js"; import { CSharpEmitterContext } from "../../src/sdk-context.js"; import { createCSharpSdkContext, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Microsoft.TypeSpec.Generator.Wasm.csproj b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Microsoft.TypeSpec.Generator.Wasm.csproj new file mode 100644 index 00000000000..c491d83f1ee --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Microsoft.TypeSpec.Generator.Wasm.csproj @@ -0,0 +1,30 @@ + + + net10.0 + browser-wasm + Exe + enable + true + + false + + $(NoWarn);SA1402;CA1416;IL2026;IL2075;IL2070;IL2104 + false + + main.js + + true + + false + true + false + + $(NoWarn);SA1402;CA1416;IL2026 + + false + + + + + + diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Program.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Program.cs new file mode 100644 index 00000000000..c108007865b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Program.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Required entry point for WASM browser app +public partial class Program +{ + public static void Main() { } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs new file mode 100644 index 00000000000..a574228c7cb --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices.JavaScript; +using System.Text.Json; +using Microsoft.TypeSpec.Generator; +using Microsoft.TypeSpec.Generator.ClientModel; +using Microsoft.TypeSpec.Generator.Input; +using Microsoft.TypeSpec.Generator.Primitives; +using Microsoft.TypeSpec.Generator.SourceInput; + +namespace Microsoft.TypeSpec.Generator.Wasm; + +public partial class WasmGenerator +{ + /// + /// Generates C# source files from a tspCodeModel.json and Configuration.json. + /// Called from JavaScript via [JSExport]. + /// + /// The tspCodeModel.json content + /// The Configuration.json content + /// JSON object mapping file paths to generated C# content + [JSExport] + public static string Generate(string codeModelJson, string configurationJson) + { + try + { + return GenerateCore(codeModelJson, configurationJson); + } + catch (Exception ex) + { + var error = new Dictionary + { + ["__error"] = ex.ToString() + }; + return JsonSerializer.Serialize(error); + } + } + + private static string GenerateCore(string codeModelJson, string configurationJson) + { + // 1. Write the code model to a temp path on the WASM virtual filesystem + // so that InputLibrary can load it normally + const string tempOutputPath = "/tmp/typespec-wasm-output"; + Directory.CreateDirectory(tempOutputPath); + File.WriteAllText(Path.Combine(tempOutputPath, "tspCodeModel.json"), codeModelJson); + + // 2. Create configuration from JSON string (internal API, accessible via InternalsVisibleTo) + var configuration = Configuration.Load(tempOutputPath, configurationJson); + + // 3. Create the generator context and instantiate the generator directly (no MEF) + var context = new GeneratorContext(configuration); + var generator = new ScmCodeModelGenerator(context); + + // 4. Set up the singleton instance + CodeModelGenerator.Instance = generator; + + // Configure() loads Roslyn metadata references from assembly paths, + // which aren't available in WASM. This is safe to skip since we don't + // run Roslyn post-processing in the playground. + try + { + generator.Configure(); + } + catch (Exception) + { + // Expected in WASM - assembly locations are empty strings + } + + // 5. Set a minimal SourceInputModel (no custom code, no baseline contract) + CodeModelGenerator.Instance.SourceInputModel = new SourceInputModel(null, null); + + // 6. Build the output library - this creates all type providers + var output = generator.OutputLibrary; + foreach (var type in output.TypeProviders) + { + type.EnsureBuilt(); + } + + // 7. Run visitors + foreach (var visitor in CodeModelGenerator.Instance.Visitors) + { + visitor.VisitLibrary(output); + } + + // 8. Generate code files directly (skip Roslyn post-processing for WASM) + var generatedFiles = new Dictionary(); + + foreach (var outputType in output.TypeProviders) + { + var writer = CodeModelGenerator.Instance.GetWriter(outputType); + var codeFile = writer.Write(); + generatedFiles[codeFile.Name] = codeFile.Content; + + // Also generate serialization providers + foreach (var serialization in outputType.SerializationProviders) + { + writer = CodeModelGenerator.Instance.GetWriter(serialization); + codeFile = writer.Write(); + generatedFiles[codeFile.Name] = codeFile.Content; + } + } + + return JsonSerializer.Serialize(generatedFiles); + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/main.js b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/main.js new file mode 100644 index 00000000000..8bf43f2f6ea --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/main.js @@ -0,0 +1 @@ +// empty - required by WasmMainJSPath but unused since we use [JSExport] diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Properties/AssemblyInfo.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Properties/AssemblyInfo.cs index cb0b9207893..8123206d3ac 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Properties/AssemblyInfo.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Properties/AssemblyInfo.cs @@ -6,4 +6,5 @@ [assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")] [assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator.Tests.Perf, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")] [assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator.Tests.Common, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")] +[assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator.Wasm, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/packages/http-client-csharp/global.json b/packages/http-client-csharp/global.json index 311edc2181a..7be862f576b 100644 --- a/packages/http-client-csharp/global.json +++ b/packages/http-client-csharp/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.103", + "version": "10.0.102", "rollForward": "feature" } } diff --git a/packages/playground-website/package.json b/packages/playground-website/package.json index 9e02692cf18..5c88cd1f4ae 100644 --- a/packages/playground-website/package.json +++ b/packages/playground-website/package.json @@ -53,12 +53,15 @@ "!dist/test/**" ], "dependencies": { + "@azure-tools/typespec-azure-core": "0.66.0", + "@azure-tools/typespec-client-generator-core": "0.66.3", "@fluentui/react-components": "catalog:", "@fluentui/react-icons": "catalog:", "@typespec/compiler": "workspace:^", "@typespec/events": "workspace:^", "@typespec/html-program-viewer": "workspace:^", "@typespec/http": "workspace:^", + "@typespec/http-client-csharp": "file:../http-client-csharp", "@typespec/json-schema": "workspace:^", "@typespec/openapi": "workspace:^", "@typespec/openapi3": "workspace:^", diff --git a/packages/playground-website/src/config.ts b/packages/playground-website/src/config.ts index ee32fdfa694..4afb8c6541f 100644 --- a/packages/playground-website/src/config.ts +++ b/packages/playground-website/src/config.ts @@ -15,6 +15,9 @@ export const TypeSpecPlaygroundConfig = { "@typespec/events", "@typespec/sse", "@typespec/xml", + "@azure-tools/typespec-azure-core", + "@azure-tools/typespec-client-generator-core", + "@typespec/http-client-csharp", ], samples, } as const; diff --git a/packages/playground-website/vite.config.ts b/packages/playground-website/vite.config.ts index dd4bc0e5a3b..162a52e3f27 100644 --- a/packages/playground-website/vite.config.ts +++ b/packages/playground-website/vite.config.ts @@ -1,5 +1,7 @@ import { definePlaygroundViteConfig } from "@typespec/playground/vite"; import { execSync } from "child_process"; +import { existsSync } from "fs"; +import { resolve } from "path"; import { visualizer } from "rollup-plugin-visualizer"; import { defineConfig, loadEnv } from "vite"; import { TypeSpecPlaygroundConfig } from "./src/config.js"; @@ -13,6 +15,12 @@ function getPrNumber() { return process.env["SYSTEM_PULLREQUEST_PULLREQUESTNUMBER"]; } +// Path to the WASM generator bundle +const wasmBundlePath = resolve( + __dirname, + "../http-client-csharp/generator/artifacts/bin/Microsoft.TypeSpec.Generator.Wasm/Release/net10.0/browser-wasm/AppBundle", +); + export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd()); const useLocalLibraries = env["VITE_USE_LOCAL_LIBRARIES"] === "true"; @@ -29,6 +37,38 @@ export default defineConfig(({ mode }) => { }) as any, ); + // Serve WASM generator bundle as static files + if (existsSync(wasmBundlePath)) { + config.server = { + ...config.server, + fs: { + ...((config.server as any)?.fs ?? {}), + allow: [...((config.server as any)?.fs?.allow ?? []), wasmBundlePath], + }, + }; + + // Add middleware to serve WASM bundle at /wasm/csharp/ + config.plugins!.push({ + name: "serve-csharp-wasm", + configureServer(server) { + server.middlewares.use("/wasm/csharp/", (req, res, next) => { + // Rewrite to serve from the AppBundle directory + const filePath = resolve(wasmBundlePath, req.url!.slice(1)); + if (existsSync(filePath)) { + return server.middlewares.handle( + Object.assign(req, { + url: `/@fs/${filePath}`, + }), + res, + next, + ); + } + next(); + }); + }, + }); + } + const prNumber = getPrNumber(); if (prNumber) { config.define = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f91e817d807..a2c2eb4040c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2080,6 +2080,12 @@ importers: packages/playground-website: dependencies: + '@azure-tools/typespec-azure-core': + specifier: 0.66.0 + version: 0.66.0(@typespec/compiler@packages+compiler)(@typespec/http@packages+http)(@typespec/rest@packages+rest) + '@azure-tools/typespec-client-generator-core': + specifier: 0.66.3 + version: 0.66.3(@azure-tools/typespec-azure-core@0.66.0(@typespec/compiler@packages+compiler)(@typespec/http@packages+http)(@typespec/rest@packages+rest))(@typespec/compiler@packages+compiler)(@typespec/events@packages+events)(@typespec/http@packages+http)(@typespec/openapi@packages+openapi)(@typespec/rest@packages+rest)(@typespec/sse@packages+sse)(@typespec/streams@packages+streams)(@typespec/versioning@packages+versioning)(@typespec/xml@packages+xml) '@fluentui/react-components': specifier: 'catalog:' version: 9.73.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(scheduler@0.27.0) @@ -2098,6 +2104,9 @@ importers: '@typespec/http': specifier: workspace:^ version: link:../http + '@typespec/http-client-csharp': + specifier: file:../http-client-csharp + version: file:packages/http-client-csharp(@azure-tools/typespec-client-generator-core@0.66.3(@azure-tools/typespec-azure-core@0.66.0(@typespec/compiler@packages+compiler)(@typespec/http@packages+http)(@typespec/rest@packages+rest))(@typespec/compiler@packages+compiler)(@typespec/events@packages+events)(@typespec/http@packages+http)(@typespec/openapi@packages+openapi)(@typespec/rest@packages+rest)(@typespec/sse@packages+sse)(@typespec/streams@packages+streams)(@typespec/versioning@packages+versioning)(@typespec/xml@packages+xml))(@typespec/compiler@packages+compiler)(@typespec/http@packages+http)(@typespec/openapi@packages+openapi)(@typespec/rest@packages+rest)(@typespec/sse@packages+sse)(@typespec/streams@packages+streams)(@typespec/versioning@packages+versioning) '@typespec/json-schema': specifier: workspace:^ version: link:../json-schema @@ -3400,6 +3409,29 @@ packages: '@azu/style-format@1.0.1': resolution: {integrity: sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==} + '@azure-tools/typespec-azure-core@0.66.0': + resolution: {integrity: sha512-OBKxRN7AucK3snh+GtLKSDdcZTz08IgcSZlIO3c4KSlmcR5twT1NMyqf1+V8SAhyOdZimndb+ikzrkkgab+GpA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@typespec/compiler': ^1.10.0 + '@typespec/http': ^1.10.0 + '@typespec/rest': ^0.80.0 + + '@azure-tools/typespec-client-generator-core@0.66.3': + resolution: {integrity: sha512-sNetQ6igxAp/vL6X2kEIy715ToDTqoJeb+OL59GEUtOW/3KBSi5tsxvDdCwSfEoaNEmv/FYjh/gJDwAWCJdFJg==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@azure-tools/typespec-azure-core': ^0.66.0 + '@typespec/compiler': ^1.10.0 + '@typespec/events': ^0.80.0 + '@typespec/http': ^1.10.0 + '@typespec/openapi': ^1.10.0 + '@typespec/rest': ^0.80.0 + '@typespec/sse': ^0.80.0 + '@typespec/streams': ^0.80.0 + '@typespec/versioning': ^0.80.0 + '@typespec/xml': ^0.80.0 + '@azure/abort-controller@2.1.2': resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} engines: {node: '>=18.0.0'} @@ -7029,6 +7061,18 @@ packages: resolution: {integrity: sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typespec/http-client-csharp@file:packages/http-client-csharp': + resolution: {directory: packages/http-client-csharp, type: directory} + peerDependencies: + '@azure-tools/typespec-client-generator-core': '>=0.66.3 <0.67.0 || ~0.67.0-0' + '@typespec/compiler': ^1.10.0 + '@typespec/http': ^1.10.0 + '@typespec/openapi': ^1.10.0 + '@typespec/rest': '>=0.80.0 <0.81.0 || ~0.81.0-0' + '@typespec/sse': '>=0.80.0 <0.81.0 || ~0.81.0-0' + '@typespec/streams': '>=0.80.0 <0.81.0 || ~0.81.0-0' + '@typespec/versioning': '>=0.80.0 <0.81.0 || ~0.81.0-0' + '@typespec/ts-http-runtime@0.3.4': resolution: {integrity: sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==} engines: {node: '>=20.0.0'} @@ -14003,6 +14047,28 @@ snapshots: dependencies: '@azu/format-text': 1.0.2 + '@azure-tools/typespec-azure-core@0.66.0(@typespec/compiler@packages+compiler)(@typespec/http@packages+http)(@typespec/rest@packages+rest)': + dependencies: + '@typespec/compiler': link:packages/compiler + '@typespec/http': link:packages/http + '@typespec/rest': link:packages/rest + + '@azure-tools/typespec-client-generator-core@0.66.3(@azure-tools/typespec-azure-core@0.66.0(@typespec/compiler@packages+compiler)(@typespec/http@packages+http)(@typespec/rest@packages+rest))(@typespec/compiler@packages+compiler)(@typespec/events@packages+events)(@typespec/http@packages+http)(@typespec/openapi@packages+openapi)(@typespec/rest@packages+rest)(@typespec/sse@packages+sse)(@typespec/streams@packages+streams)(@typespec/versioning@packages+versioning)(@typespec/xml@packages+xml)': + dependencies: + '@azure-tools/typespec-azure-core': 0.66.0(@typespec/compiler@packages+compiler)(@typespec/http@packages+http)(@typespec/rest@packages+rest) + '@typespec/compiler': link:packages/compiler + '@typespec/events': link:packages/events + '@typespec/http': link:packages/http + '@typespec/openapi': link:packages/openapi + '@typespec/rest': link:packages/rest + '@typespec/sse': link:packages/sse + '@typespec/streams': link:packages/streams + '@typespec/versioning': link:packages/versioning + '@typespec/xml': link:packages/xml + change-case: 5.4.4 + pluralize: 8.0.0 + yaml: 2.8.3 + '@azure/abort-controller@2.1.2': dependencies: tslib: 2.8.1 @@ -18990,6 +19056,17 @@ snapshots: '@typescript-eslint/types': 8.57.0 eslint-visitor-keys: 5.0.1 + '@typespec/http-client-csharp@file:packages/http-client-csharp(@azure-tools/typespec-client-generator-core@0.66.3(@azure-tools/typespec-azure-core@0.66.0(@typespec/compiler@packages+compiler)(@typespec/http@packages+http)(@typespec/rest@packages+rest))(@typespec/compiler@packages+compiler)(@typespec/events@packages+events)(@typespec/http@packages+http)(@typespec/openapi@packages+openapi)(@typespec/rest@packages+rest)(@typespec/sse@packages+sse)(@typespec/streams@packages+streams)(@typespec/versioning@packages+versioning)(@typespec/xml@packages+xml))(@typespec/compiler@packages+compiler)(@typespec/http@packages+http)(@typespec/openapi@packages+openapi)(@typespec/rest@packages+rest)(@typespec/sse@packages+sse)(@typespec/streams@packages+streams)(@typespec/versioning@packages+versioning)': + dependencies: + '@azure-tools/typespec-client-generator-core': 0.66.3(@azure-tools/typespec-azure-core@0.66.0(@typespec/compiler@packages+compiler)(@typespec/http@packages+http)(@typespec/rest@packages+rest))(@typespec/compiler@packages+compiler)(@typespec/events@packages+events)(@typespec/http@packages+http)(@typespec/openapi@packages+openapi)(@typespec/rest@packages+rest)(@typespec/sse@packages+sse)(@typespec/streams@packages+streams)(@typespec/versioning@packages+versioning)(@typespec/xml@packages+xml) + '@typespec/compiler': link:packages/compiler + '@typespec/http': link:packages/http + '@typespec/openapi': link:packages/openapi + '@typespec/rest': link:packages/rest + '@typespec/sse': link:packages/sse + '@typespec/streams': link:packages/streams + '@typespec/versioning': link:packages/versioning + '@typespec/ts-http-runtime@0.3.4': dependencies: http-proxy-agent: 7.0.2 From cfa653d4be30cd405eb2aec9c929771810ba39b7 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 2 Apr 2026 17:58:49 -0400 Subject: [PATCH 2/3] post process? --- .../src/WasmGenerator.cs | 134 +++++++++++++++++- 1 file changed, 128 insertions(+), 6 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs index a574228c7cb..b998e45485b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs @@ -4,8 +4,14 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; +using System.Reflection.Metadata; using System.Runtime.InteropServices.JavaScript; using System.Text.Json; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; using Microsoft.TypeSpec.Generator; using Microsoft.TypeSpec.Generator.ClientModel; using Microsoft.TypeSpec.Generator.Input; @@ -40,6 +46,107 @@ public static string Generate(string codeModelJson, string configurationJson) } } + /// + /// Creates a MetadataReference from a loaded assembly using in-memory metadata, + /// avoiding file system access which isn't available in WASM. + /// + private static unsafe MetadataReference? CreateMetadataReferenceFromAssembly(Assembly assembly) + { + if (assembly.TryGetRawMetadata(out byte* blob, out int length)) + { + var moduleMetadata = ModuleMetadata.CreateFromMetadata((IntPtr)blob, length); + var assemblyMetadata = AssemblyMetadata.Create(moduleMetadata); + return assemblyMetadata.GetReference(); + } + return null; + } + + /// + /// Collects metadata references from all loaded assemblies for Roslyn compilation. + /// + private static List CollectMetadataReferences() + { + var references = new List(); + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (assembly.IsDynamic) + continue; + try + { + var reference = CreateMetadataReferenceFromAssembly(assembly); + if (reference != null) + references.Add(reference); + } + catch + { + // Skip assemblies that can't provide metadata + } + } + return references; + } + + /// + /// Runs Roslyn simplification and formatting on generated C# code. + /// + private static async System.Threading.Tasks.Task> PostProcessFilesAsync( + Dictionary generatedFiles) + { + var metadataReferences = CollectMetadataReferences(); + + using var workspace = new AdhocWorkspace(); + var projectId = ProjectId.CreateNewId(); + var projectInfo = ProjectInfo.Create( + projectId, + VersionStamp.Create(), + "GeneratedCode", + "GeneratedCode", + LanguageNames.CSharp, + compilationOptions: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary), + metadataReferences: metadataReferences); + + var project = workspace.AddProject(projectInfo); + + // Add all generated files as documents + var documentMap = new Dictionary(); + foreach (var (filePath, content) in generatedFiles) + { + var document = project.AddDocument(filePath, Microsoft.CodeAnalysis.Text.SourceText.From(content)); + documentMap[document.Id] = filePath; + project = document.Project; + } + + // Apply the updated project to the workspace + workspace.TryApplyChanges(project.Solution); + project = workspace.CurrentSolution.GetProject(projectId)!; + + // Process each document: simplify and format + var processed = new Dictionary(); + + foreach (var document in project.Documents) + { + if (!documentMap.ContainsKey(document.Id)) + continue; + + try + { + // Run Roslyn simplification (reduces qualified names like System.String → string) + var simplified = await Simplifier.ReduceAsync(document); + // Run Roslyn formatting + var formatted = await Formatter.FormatAsync(simplified); + var text = await formatted.GetTextAsync(); + processed[documentMap[document.Id]] = text.ToString(); + } + catch + { + // If post-processing fails for a file, use the raw output + var text = await document.GetTextAsync(); + processed[documentMap[document.Id]] = text.ToString(); + } + } + + return processed; + } + private static string GenerateCore(string codeModelJson, string configurationJson) { // 1. Write the code model to a temp path on the WASM virtual filesystem @@ -55,19 +162,24 @@ private static string GenerateCore(string codeModelJson, string configurationJso var context = new GeneratorContext(configuration); var generator = new ScmCodeModelGenerator(context); - // 4. Set up the singleton instance + // 4. Set up the singleton instance and configure with in-memory metadata references CodeModelGenerator.Instance = generator; - // Configure() loads Roslyn metadata references from assembly paths, - // which aren't available in WASM. This is safe to skip since we don't - // run Roslyn post-processing in the playground. + // Configure the generator, catching metadata reference errors that occur in WASM + // (Assembly.Location is empty in WASM, but we handle references separately) try { generator.Configure(); } catch (Exception) { - // Expected in WASM - assembly locations are empty strings + // Expected - ScmCodeModelGenerator.Configure() tries CreateFromFile with empty paths + } + + // Add metadata references from in-memory assemblies (works in WASM) + foreach (var reference in CollectMetadataReferences()) + { + generator.AddMetadataReference(reference); } // 5. Set a minimal SourceInputModel (no custom code, no baseline contract) @@ -86,7 +198,7 @@ private static string GenerateCore(string codeModelJson, string configurationJso visitor.VisitLibrary(output); } - // 8. Generate code files directly (skip Roslyn post-processing for WASM) + // 8. Generate raw code files var generatedFiles = new Dictionary(); foreach (var outputType in output.TypeProviders) @@ -104,6 +216,16 @@ private static string GenerateCore(string codeModelJson, string configurationJso } } + // 9. Run Roslyn post-processing (simplification + formatting) + try + { + generatedFiles = PostProcessFilesAsync(generatedFiles).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[WASM] Roslyn post-processing failed, using raw output: {ex.Message}"); + } + return JsonSerializer.Serialize(generatedFiles); } } From 5a4866609950572c2836e80a911f5881ae6a8457 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 2 Apr 2026 18:48:05 -0400 Subject: [PATCH 3/3] workers --- .../emitter/src/lib/wasm-generator.ts | 39 ++----------- .../Microsoft.TypeSpec.Generator.Wasm.csproj | 5 ++ .../src/WasmGenerator.cs | 28 ++++++--- .../src/worker.js | 58 +++++++++++++++++++ packages/playground-website/vite.config.ts | 43 +++++++------- 5 files changed, 107 insertions(+), 66 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/worker.js diff --git a/packages/http-client-csharp/emitter/src/lib/wasm-generator.ts b/packages/http-client-csharp/emitter/src/lib/wasm-generator.ts index ca80ea45778..67d20dba3fb 100644 --- a/packages/http-client-csharp/emitter/src/lib/wasm-generator.ts +++ b/packages/http-client-csharp/emitter/src/lib/wasm-generator.ts @@ -3,19 +3,14 @@ /** * Bridge between the TypeSpec JS emitter and the C# WASM generator. - * This module lazily loads the .NET WASM runtime and calls the C# generator + * Lazily loads the .NET WASM runtime and calls the C# generator * to produce generated C# files from the tspCodeModel.json intermediate representation. */ let wasmExports: any = null; let initPromise: Promise | null = null; -/** - * Get the base URL for the WASM bundle assets. - * In the playground, these are served from a configurable path. - */ function getWasmBaseUrl(): string { - // Look for a global configuration, fall back to a default path return (globalThis as any).__TYPESPEC_CSHARP_WASM_BASE_URL ?? "/wasm/csharp/"; } @@ -25,18 +20,11 @@ async function initializeWasm(): Promise { const baseUrl = getWasmBaseUrl(); const dotnetUrl = `${baseUrl}_framework/dotnet.js`; - // Dynamically import the .NET WASM runtime + console.log("[http-client-csharp] Loading .NET WASM runtime..."); const { dotnet } = await import(/* @vite-ignore */ dotnetUrl); - // Configure the runtime to load assets from the correct base URL - const { getAssemblyExports, getConfig } = await dotnet - .withModuleConfig({ - locateFile: (path: string) => `${baseUrl}_framework/${path}`, - }) - .withResourceLoader((_type: string, name: string, _defaultUri: string) => { - return `${baseUrl}_framework/${name}`; - }) - .create(); + const { getAssemblyExports, getConfig } = await dotnet.create(); + console.log("[http-client-csharp] .NET WASM runtime initialized"); const config = getConfig(); wasmExports = await getAssemblyExports(config.mainAssemblyName); @@ -44,16 +32,11 @@ async function initializeWasm(): Promise { /** * Generate C# source files from code model JSON using the WASM generator. - * - * @param codeModelJson - The tspCodeModel.json content - * @param configurationJson - The Configuration.json content - * @returns A map of file paths to generated C# content */ export async function generateCSharpFromWasm( codeModelJson: string, configurationJson: string, ): Promise> { - // Lazy-initialize the WASM runtime if (!initPromise) { initPromise = initializeWasm(); } @@ -63,7 +46,6 @@ export async function generateCSharpFromWasm( throw new Error("Failed to initialize the C# WASM generator"); } - // Call the [JSExport] method const resultJson = wasmExports.Microsoft.TypeSpec.Generator.Wasm.WasmGenerator.Generate( codeModelJson, @@ -72,7 +54,6 @@ export async function generateCSharpFromWasm( const result: Record = JSON.parse(resultJson); - // Check for errors from the C# side if (result["__error"]) { throw new Error(`C# generator error: ${result["__error"]}`); } @@ -80,15 +61,3 @@ export async function generateCSharpFromWasm( return result; } -/** - * Check if the WASM generator is available (i.e., the assets are served). - */ -export async function isWasmGeneratorAvailable(): Promise { - try { - const baseUrl = getWasmBaseUrl(); - const response = await fetch(`${baseUrl}_framework/dotnet.js`, { method: "HEAD" }); - return response.ok; - } catch { - return false; - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Microsoft.TypeSpec.Generator.Wasm.csproj b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Microsoft.TypeSpec.Generator.Wasm.csproj index c491d83f1ee..ad57deeb74e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Microsoft.TypeSpec.Generator.Wasm.csproj +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/Microsoft.TypeSpec.Generator.Wasm.csproj @@ -27,4 +27,9 @@ + + + + + diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs index b998e45485b..b40c373189b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/WasmGenerator.cs @@ -31,10 +31,17 @@ public partial class WasmGenerator /// JSON object mapping file paths to generated C# content [JSExport] public static string Generate(string codeModelJson, string configurationJson) + { + return Generate(codeModelJson, configurationJson, false); + } + + /// Enable Roslyn simplification/formatting (slower but prettier output) + [JSExport] + public static string Generate(string codeModelJson, string configurationJson, bool enablePostProcessing) { try { - return GenerateCore(codeModelJson, configurationJson); + return GenerateCore(codeModelJson, configurationJson, enablePostProcessing); } catch (Exception ex) { @@ -147,7 +154,7 @@ private static async System.Threading.Tasks.Task> Pos return processed; } - private static string GenerateCore(string codeModelJson, string configurationJson) + private static string GenerateCore(string codeModelJson, string configurationJson, bool enablePostProcessing) { // 1. Write the code model to a temp path on the WASM virtual filesystem // so that InputLibrary can load it normally @@ -216,14 +223,17 @@ private static string GenerateCore(string codeModelJson, string configurationJso } } - // 9. Run Roslyn post-processing (simplification + formatting) - try + // 9. Optionally run Roslyn post-processing (simplification + formatting) + if (enablePostProcessing) { - generatedFiles = PostProcessFilesAsync(generatedFiles).GetAwaiter().GetResult(); - } - catch (Exception ex) - { - Console.Error.WriteLine($"[WASM] Roslyn post-processing failed, using raw output: {ex.Message}"); + try + { + generatedFiles = PostProcessFilesAsync(generatedFiles).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[WASM] Roslyn post-processing failed, using raw output: {ex.Message}"); + } } return JsonSerializer.Serialize(generatedFiles); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/worker.js b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/worker.js new file mode 100644 index 00000000000..e36d5755396 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.Wasm/src/worker.js @@ -0,0 +1,58 @@ +// Web Worker that runs the C# WASM generator off the main thread. +// Loaded from /wasm/csharp/worker.js + +let wasmExports = null; +let initPromise = null; + +async function initializeWasm() { + if (wasmExports) return; + + console.log("[wasm-worker] Importing dotnet.js..."); + const { dotnet } = await import("./_framework/dotnet.js"); + console.log("[wasm-worker] dotnet.js imported, calling dotnet.create()..."); + + // Configure the runtime to locate assets relative to the worker's URL + const { getAssemblyExports, getConfig } = await dotnet + .withModuleConfig({ + locateFile: (path) => `./_framework/${path}`, + }) + .create(); + console.log("[wasm-worker] .NET runtime created"); + const config = getConfig(); + wasmExports = await getAssemblyExports(config.mainAssemblyName); + console.log("[wasm-worker] Assembly exports loaded"); +} + +self.onmessage = async (e) => { + const { id, codeModelJson, configurationJson } = e.data; + console.log("[wasm-worker] Received request id:", id); + + try { + if (!initPromise) { + initPromise = initializeWasm(); + } + await initPromise; + console.log("[wasm-worker] Calling Generate..."); + + const resultJson = + wasmExports.Microsoft.TypeSpec.Generator.Wasm.WasmGenerator.Generate( + codeModelJson, + configurationJson, + ); + + const result = JSON.parse(resultJson); + console.log("[wasm-worker] Generate complete, files:", Object.keys(result).length); + + if (result["__error"]) { + self.postMessage({ id, error: result["__error"] }); + } else { + self.postMessage({ id, files: result }); + } + } catch (err) { + console.error("[wasm-worker] Error:", err); + self.postMessage({ id, error: err.toString() }); + } +}; + +// Signal that the worker is ready +self.postMessage({ type: "ready" }); diff --git a/packages/playground-website/vite.config.ts b/packages/playground-website/vite.config.ts index 162a52e3f27..3a673c5c0bc 100644 --- a/packages/playground-website/vite.config.ts +++ b/packages/playground-website/vite.config.ts @@ -1,6 +1,6 @@ import { definePlaygroundViteConfig } from "@typespec/playground/vite"; import { execSync } from "child_process"; -import { existsSync } from "fs"; +import { existsSync, readFileSync, statSync } from "fs"; import { resolve } from "path"; import { visualizer } from "rollup-plugin-visualizer"; import { defineConfig, loadEnv } from "vite"; @@ -37,31 +37,30 @@ export default defineConfig(({ mode }) => { }) as any, ); - // Serve WASM generator bundle as static files + // Serve WASM generator bundle as static files (bypassing Vite transforms) if (existsSync(wasmBundlePath)) { - config.server = { - ...config.server, - fs: { - ...((config.server as any)?.fs ?? {}), - allow: [...((config.server as any)?.fs?.allow ?? []), wasmBundlePath], - }, - }; - - // Add middleware to serve WASM bundle at /wasm/csharp/ config.plugins!.push({ name: "serve-csharp-wasm", configureServer(server) { - server.middlewares.use("/wasm/csharp/", (req, res, next) => { - // Rewrite to serve from the AppBundle directory - const filePath = resolve(wasmBundlePath, req.url!.slice(1)); - if (existsSync(filePath)) { - return server.middlewares.handle( - Object.assign(req, { - url: `/@fs/${filePath}`, - }), - res, - next, - ); + // Serve WASM bundle files raw — must bypass Vite's transform pipeline + // because dotnet.js/dotnet.runtime.js break when Vite injects its client code + server.middlewares.use("/wasm/csharp", (req, res, next) => { + const urlPath = req.url?.split("?")[0] ?? ""; + const filePath = resolve(wasmBundlePath, urlPath.startsWith("/") ? urlPath.slice(1) : urlPath); + if (existsSync(filePath) && !statSync(filePath).isDirectory()) { + const content = readFileSync(filePath); + const ext = filePath.split(".").pop(); + const mimeTypes: Record = { + js: "application/javascript", + mjs: "application/javascript", + wasm: "application/wasm", + json: "application/json", + dat: "application/octet-stream", + }; + res.setHeader("Content-Type", mimeTypes[ext ?? ""] ?? "application/octet-stream"); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.end(content); + return; } next(); });