Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4301e90
feat: enable C# emitter in playground with backend server
JoshLove-msft Apr 12, 2026
437b2ca
fix: preserve EsrpRelease in emitter-stages, add playground server de…
JoshLove-msft Apr 12, 2026
da56524
fix: restore plugin path resolution in emitter.ts
JoshLove-msft Apr 12, 2026
b07b370
fix: prettier formatting, cspell words, lint cleanup
JoshLove-msft Apr 12, 2026
a6006ff
fix: clarify browser stub comment
JoshLove-msft Apr 12, 2026
f03059f
fix: address PR review feedback
JoshLove-msft Apr 13, 2026
ef6e306
style: format emit-generate.browser.ts
JoshLove-msft Apr 13, 2026
f03f775
test: add tests for browser stub (emit-generate.browser)
JoshLove-msft Apr 13, 2026
a7c273b
fix: add missing GenerateOptions properties in browser test
JoshLove-msft Apr 13, 2026
01c4644
Merge remote-tracking branch 'upstream/main' into feat/csharp-playgro…
JoshLove-msft Apr 13, 2026
99e0ee5
fix: require explicit server URL configuration
JoshLove-msft Apr 13, 2026
fadf720
fix: hardcode server URL in emitter, remove configurability
JoshLove-msft Apr 13, 2026
664aff5
fix: use Node 22 for playground bundle upload in publish stage
JoshLove-msft Apr 13, 2026
5780707
fix: stamp build version on package.json before playground bundle upload
JoshLove-msft Apr 14, 2026
ed629d0
fix: use BUILD_BUILDNUMBER env var in version stamp script
JoshLove-msft Apr 14, 2026
b4d53db
fix: use alpha/beta version tag based on BuildPrereleaseVersion
JoshLove-msft Apr 14, 2026
700d1b1
style: format emitter-stages.yml
JoshLove-msft Apr 14, 2026
5cb4659
feat: add request/response validation and security hardening
JoshLove-msft Apr 15, 2026
bd34a3c
fix: add headers to mock responses in browser stub tests
JoshLove-msft Apr 15, 2026
cf88a0a
feat: add response size limit check in browser emitter
JoshLove-msft Apr 17, 2026
2d85b12
Merge remote-tracking branch 'upstream/main' into feat/csharp-playgro…
JoshLove-msft Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ words:
- CORGE
- createsorreplacesresource
- createsorupdatesresource
- Creds
- CRUDL
- ctxt
- dbaeumer
Expand Down Expand Up @@ -270,6 +271,7 @@ words:
- tspwebsitepr
- tsvs
- typespec
- typespecacr
- typespecvs
- tzname
- Uhoh
Expand Down
72 changes: 71 additions & 1 deletion eng/emitters/pipelines/templates/stages/emitter-stages.yml
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any emitter option that allow users to run a post emitting script that would allow a playground user to basically run code on the server?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the closest thing is there an option to configure a plugin but the plugin would need to exist on the server.

Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ parameters:
type: boolean
default: false

# The npm script to run to build the emitter for playground bundling.
# Default is "build". C# uses "build:emitter" to skip the .NET generator build.
- name: PlaygroundBundleBuildScript
type: string
default: "build"

# Path to a Dockerfile (relative to PackagePath) for the playground server.
# When set alongside UploadPlaygroundBundle, the server container is built and
# deployed to Azure App Service after publishing.
- name: PlaygroundServerDockerfile
type: string
default: ""

# Azure App Service name for the playground server.
- name: PlaygroundServerAppName
type: string
default: ""

stages:
# Build stage
# Responsible for building the autorest generator and typespec emitter packages
Expand Down Expand Up @@ -353,10 +371,30 @@ stages:
LanguageShortName: ${{ parameters.LanguageShortName }}

- ${{ if parameters.UploadPlaygroundBundle }}:
- task: NodeTool@0
displayName: Use Node 22.x for playground bundle
inputs:
versionSpec: "22.x"
- script: npm ci
displayName: Install emitter dependencies for playground bundle
workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }}
- script: npm run build
- ${{ if parameters.BuildPrereleaseVersion }}:
- script: |
CURRENT_VERSION=$(node -p "require('./package.json').version")
NEW_VERSION="${CURRENT_VERSION}-alpha.${BUILD_BUILDNUMBER}"
echo "Setting version to $NEW_VERSION"
npm version "$NEW_VERSION" --no-git-tag-version
displayName: Stamp build version for playground bundle
workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }}
- ${{ else }}:
- script: |
CURRENT_VERSION=$(node -p "require('./package.json').version")
NEW_VERSION="${CURRENT_VERSION}-beta.${BUILD_BUILDNUMBER}"
echo "Setting version to $NEW_VERSION"
npm version "$NEW_VERSION" --no-git-tag-version
displayName: Stamp build version for playground bundle
workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }}
- script: npm run ${{ parameters.PlaygroundBundleBuildScript }}
displayName: Build emitter for playground bundle
workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PackagePath }}
- script: npm install -g pnpm
Expand All @@ -375,6 +413,38 @@ stages:
inlineScript: node ./eng/emitters/scripts/upload-bundled-emitter.js ${{ parameters.PackagePath }}
workingDirectory: $(Build.SourcesDirectory)

- ${{ if and(parameters.PlaygroundServerDockerfile, parameters.PlaygroundServerAppName) }}:
Comment thread
JoshLove-msft marked this conversation as resolved.
- task: AzureCLI@1
displayName: Build and deploy playground server
inputs:
azureSubscription: "Azure SDK Engineering System"
Comment thread
JoshLove-msft marked this conversation as resolved.
scriptLocation: inlineScript
inlineScript: |
set -e
REGISTRY="typespecacr"
RESOURCE_GROUP="typespec"
APP_NAME="${{ parameters.PlaygroundServerAppName }}"
IMAGE="$REGISTRY.azurecr.io/$APP_NAME:$(Build.BuildId)"

# Build and push Docker image to ACR
echo "Building Docker image: $IMAGE"
CONTEXT="$(Build.SourcesDirectory)/${{ parameters.PackagePath }}"
az acr build \
--registry "$REGISTRY" \
--image "$APP_NAME:$(Build.BuildId)" \
--file "$CONTEXT/${{ parameters.PlaygroundServerDockerfile }}" \
"$CONTEXT"

# Update App Service container image
az webapp config container set \
--name "$APP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--container-image-name "$IMAGE" \
--container-registry-url "https://$REGISTRY.azurecr.io"

echo "Deployed to https://$APP_NAME.azurewebsites.net"
workingDirectory: $(Build.SourcesDirectory)

templateContext:
outputs:
- output: pipelineArtifact
Expand Down
8 changes: 8 additions & 0 deletions packages/http-client-csharp/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules/
Comment thread
JoshLove-msft marked this conversation as resolved.
dist/
emitter/
.tspd/
**/artifacts/
*.md
*.tsp
package-lock.json
12 changes: 11 additions & 1 deletion packages/http-client-csharp/emitter/src/code-model-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import { CSharpEmitterContext } from "./sdk-context.js";
import { CodeModel } from "./type/code-model.js";
import { Configuration } from "./type/configuration.js";

/**
* Serializes the code model to a JSON string with reference tracking.
* @param context - The CSharp emitter context
* @param codeModel - The code model to serialize
* @beta
*/
export function serializeCodeModel(context: CSharpEmitterContext, codeModel: CodeModel): string {
return prettierOutput(JSON.stringify(buildJson(context, codeModel), transformJSONProperties, 2));
}

/**
* Writes the code model to the output folder. Should only be used by autorest.csharp.
* @param context - The CSharp emitter context
Expand All @@ -22,7 +32,7 @@ export async function writeCodeModel(
) {
await context.program.host.writeFile(
resolvePath(outputFolder, tspOutputFileName),
prettierOutput(JSON.stringify(buildJson(context, codeModel), transformJSONProperties, 2)),
serializeCodeModel(context, codeModel),
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

// Browser implementation: sends code model to a playground server for generation.

import { resolvePath } from "@typespec/compiler";
import type { GenerateOptions } from "./emit-generate.js";
import { CSharpEmitterContext } from "./sdk-context.js";

const SERVER_URL = "https://csharp-playground-server.azurewebsites.net";
const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10 MB

export async function generate(
sdkContext: CSharpEmitterContext,
codeModelJson: string,
configJson: string,
options: GenerateOptions,
): Promise<void> {
const response = await fetch(`${SERVER_URL}/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
codeModel: codeModelJson,
configuration: configJson,
generatorName: options.generatorName,
}),
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(`Playground server error (${response.status}): ${errorText}`);
}

const contentType = response.headers.get("content-type");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering about a limit on response size? content-length is not a great way to police this, but not sure what size quotas are available to use

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a Content-Length check against a 10 MB limit before parsing the response JSON. As you noted, Content-Length isn't always reliable, but it provides a reasonable guard against obviously oversized responses without buffering the entire body.

-- Generated by Copilot

if (!contentType?.includes("application/json")) {
throw new Error(`Unexpected response content-type: ${contentType}`);
}

const contentLength = response.headers.get("content-length");
if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
throw new Error(
`Response too large: ${contentLength} bytes exceeds ${MAX_RESPONSE_SIZE} byte limit`,
);
}

const result = await response.json();

if (!result || !Array.isArray(result.files)) {
throw new Error("Invalid response: expected { files: [...] }");
}

for (const file of result.files) {
if (typeof file.path !== "string" || typeof file.content !== "string") {
throw new Error(`Invalid file entry: expected { path: string, content: string }`);
}
await sdkContext.program.host.writeFile(
resolvePath(options.outputFolder, file.path),
file.content,
);
}
}
Loading
Loading