Repository: Azure/typespec-azure
Pull request #2447: [tcgc] emitter options split
Author: login: iscai-msft
Created: 2025-03-25T21:27:39Z | Merged: 2025-03-28T17:39:37Z

BREAKING: emitter options will now be individually exposed and split into `BrandedSdkEmitterOptions` and `UnbrandedSdkEmitterOptions`. This change allows emitters to choose which flags to support, BUT it means that you will have to explicitly choose the flags to support. Example code snippet in an emitter:

```ts
import { BrandedSdkEmitterOptions, UnbrandedSdkEmitterOptions } from "@azure-tools/typespec-client-generator-core"

const UnbrandedPythonEmitterOptionsSchema: JSONSchemaType<UnbrandedPythonEmitterOptions> = {
  type: "object",
  additionalProperties: true,
  properties: {
    "python-specific-flag": {...},
    ...UnbrandedSdkEmitterOptions["api-version"],
    ...UnbrandedSdkEmitterOptions["license"],
  }
}

const BrandedPythonEmitterOptionsSchema: JSONSchemaType<BrandedPythonEmitterOptions> = {
  type: "object",
  additionalProperties: true,
  properties: {
    "branded-python-specific-flag": {...},
    ...BrandedSdkEmitterOptions["namespace"],
    ...UnbrandedPythonEmitterOptionsSchema.properties,
  }
}
```


Diff:
diff --git a/.chronus/changes/tcgc-emitterOptionsSplit-2025-2-25-17-26-43.md b/.chronus/changes/tcgc-emitterOptionsSplit-2025-2-25-17-26-43.md
new file mode 100644
index 0000000000..d491861401
--- /dev/null
+++ b/.chronus/changes/tcgc-emitterOptionsSplit-2025-2-25-17-26-43.md
@@ -0,0 +1,7 @@
+---
+changeKind: breaking
+packages:
+  - "@azure-tools/typespec-client-generator-core"
+---
+
+Split emitter options into `UnbrandedSdkEmitterOptions` and `BrandedSdkEmitterOptions`. Each flag will be exported individually, so emitters can choose which flags to support
diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md
index ca48cc3bb9..175bf62b1b 100644
--- a/packages/typespec-client-generator-core/README.md
+++ b/packages/typespec-client-generator-core/README.md
@@ -53,29 +53,29 @@ When set to `true`, the emitter will generate low-level protocol methods for eac
 
 When set to `true`, the emitter will generate low-level protocol methods for each service operation if `@convenientAPI` is not set for an operation. Default value is `true`.
 
-### `examples-dir`
+### `api-version`
 
 **Type:** `string`
 
-Specifies the directory where the emitter will look for example files. If the flag isn’t set, the emitter defaults to using an `examples` directory located at the project root.
+Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`.
 
-### `namespace`
+### `license`
 
-**Type:** `string`
+**Type:** `object`
 
-Specifies the namespace you want to override for namespaces set in the spec. With this config, all namespace for the spec types will default to it.
+License information for the generated client code.
 
-### `api-version`
+### `examples-dir`
 
 **Type:** `string`
 
-Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`.
+Specifies the directory where the emitter will look for example files. If the flag isn’t set, the emitter defaults to using an `examples` directory located at the project root.
 
-### `license`
+### `namespace`
 
-**Type:** `object`
+**Type:** `string`
 
-License information for the generated client code.
+Specifies the namespace you want to override for namespaces set in the spec. With this config, all namespace for the spec types will default to it.
 
 ## Usage
 
diff --git a/packages/typespec-client-generator-core/src/context.ts b/packages/typespec-client-generator-core/src/context.ts
index 4b06bf2d61..9ae6cdbd53 100644
--- a/packages/typespec-client-generator-core/src/context.ts
+++ b/packages/typespec-client-generator-core/src/context.ts
@@ -28,31 +28,14 @@ import {
   TCGCContext,
 } from "./interfaces.js";
 import {
+  BrandedSdkEmitterOptionsInterface,
   handleVersioningMutationForGlobalNamespace,
   parseEmitterName,
+  TCGCEmitterOptions,
   TspLiteralType,
 } from "./internal-utils.js";
 import { getSdkPackage } from "./package.js";
 
-export interface TCGCEmitterOptions extends SdkEmitterOptions {
-  "emitter-name"?: string;
-}
-
-export interface SdkEmitterOptions {
-  "generate-protocol-methods"?: boolean;
-  "generate-convenience-methods"?: boolean;
-  "api-version"?: string;
-  "examples-dir"?: string;
-  namespace?: string;
-  license?: {
-    name: string;
-    company?: string;
-    link?: string;
-    header?: string;
-    description?: string;
-  };
-}
-
 export function createTCGCContext(program: Program, emitterName?: string): TCGCContext {
   const diagnostics = createDiagnosticCollector();
   return {
@@ -117,7 +100,7 @@ export interface CreateSdkContextOptions {
 }
 
 export async function createSdkContext<
-  TOptions extends Record<string, any> = SdkEmitterOptions,
+  TOptions extends Record<string, any> = BrandedSdkEmitterOptionsInterface,
   TServiceOperation extends SdkServiceOperation = SdkHttpOperation,
 >(
   context: EmitContext<TOptions>,
diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts
index a8ab4b2345..a77e83f9ff 100644
--- a/packages/typespec-client-generator-core/src/internal-utils.ts
+++ b/packages/typespec-client-generator-core/src/internal-utils.ts
@@ -63,6 +63,28 @@ import { getClientTypeWithDiagnostics } from "./types.js";
 
 import { $ } from "@typespec/compiler/experimental/typekit";
 
+export interface TCGCEmitterOptions extends BrandedSdkEmitterOptionsInterface {
+  "emitter-name"?: string;
+}
+
+export interface UnbrandedSdkEmitterOptionsInterface {
+  "generate-protocol-methods"?: boolean;
+  "generate-convenience-methods"?: boolean;
+  "api-version"?: string;
+  license?: {
+    name: string;
+    company?: string;
+    link?: string;
+    header?: string;
+    description?: string;
+  };
+}
+
+export interface BrandedSdkEmitterOptionsInterface extends UnbrandedSdkEmitterOptionsInterface {
+  "examples-dir"?: string;
+  namespace?: string;
+}
+
 export const AllScopes = Symbol.for("@azure-core/typespec-client-generator-core/all-scopes");
 
 export const clientNameKey = createStateSymbol("clientName");
diff --git a/packages/typespec-client-generator-core/src/lib.ts b/packages/typespec-client-generator-core/src/lib.ts
index a57a1bfa9a..1c10ffee71 100644
--- a/packages/typespec-client-generator-core/src/lib.ts
+++ b/packages/typespec-client-generator-core/src/lib.ts
@@ -1,41 +1,36 @@
 import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/compiler";
-import { SdkEmitterOptions, TCGCEmitterOptions } from "./context.js";
+import {
+  BrandedSdkEmitterOptionsInterface,
+  TCGCEmitterOptions,
+  UnbrandedSdkEmitterOptionsInterface,
+} from "./internal-utils.js";
 
-export const SdkEmitterOptionsSchema: JSONSchemaType<SdkEmitterOptions> = {
-  type: "object",
-  additionalProperties: false,
-  properties: {
+export const UnbrandedSdkEmitterOptions = {
+  "generate-protocol-methods": {
     "generate-protocol-methods": {
       type: "boolean",
       nullable: true,
       description:
         "When set to `true`, the emitter will generate low-level protocol methods for each service operation if `@protocolAPI` is not set for an operation. Default value is `true`.",
     },
+  },
+  "generate-convenience-methods": {
     "generate-convenience-methods": {
       type: "boolean",
       nullable: true,
       description:
         "When set to `true`, the emitter will generate low-level protocol methods for each service operation if `@convenientAPI` is not set for an operation. Default value is `true`.",
     },
-    "examples-dir": {
-      type: "string",
-      nullable: true,
-      format: "absolute-path",
-      description:
-        "Specifies the directory where the emitter will look for example files. If the flag isn’t set, the emitter defaults to using an `examples` directory located at the project root.",
-    },
-    namespace: {
-      type: "string",
-      nullable: true,
-      description:
-        "Specifies the namespace you want to override for namespaces set in the spec. With this config, all namespace for the spec types will default to it.",
-    },
+  },
+  "api-version": {
     "api-version": {
       type: "string",
       nullable: true,
       description:
         "Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`.",
     },
+  },
+  license: {
     license: {
       type: "object",
       additionalProperties: false,
@@ -73,6 +68,48 @@ export const SdkEmitterOptionsSchema: JSONSchemaType<SdkEmitterOptions> = {
       description: "License information for the generated client code.",
     },
   },
+} as const;
+
+const UnbrandedSdkEmitterOptionsInterfaceSchema: JSONSchemaType<UnbrandedSdkEmitterOptionsInterface> =
+  {
+    type: "object",
+    additionalProperties: false,
+    properties: {
+      ...UnbrandedSdkEmitterOptions["generate-protocol-methods"],
+      ...UnbrandedSdkEmitterOptions["generate-convenience-methods"],
+      ...UnbrandedSdkEmitterOptions["api-version"],
+      ...UnbrandedSdkEmitterOptions["license"],
+    },
+  };
+
+export const BrandedSdkEmitterOptions = {
+  "examples-dir": {
+    "examples-dir": {
+      type: "string",
+      nullable: true,
+      format: "absolute-path",
+      description:
+        "Specifies the directory where the emitter will look for example files. If the flag isn’t set, the emitter defaults to using an `examples` directory located at the project root.",
+    },
+  },
+  namespace: {
+    namespace: {
+      type: "string",
+      nullable: true,
+      description:
+        "Specifies the namespace you want to override for namespaces set in the spec. With this config, all namespace for the spec types will default to it.",
+    },
+  },
+} as const;
+
+const BrandedSdkEmitterOptionsSchema: JSONSchemaType<BrandedSdkEmitterOptionsInterface> = {
+  type: "object",
+  additionalProperties: false,
+  properties: {
+    ...UnbrandedSdkEmitterOptionsInterfaceSchema.properties!,
+    ...BrandedSdkEmitterOptions["examples-dir"],
+    ...BrandedSdkEmitterOptions["namespace"],
+  },
 };
 
 const TCGCEmitterOptionsSchema: JSONSchemaType<TCGCEmitterOptions> = {
@@ -84,7 +121,7 @@ const TCGCEmitterOptionsSchema: JSONSchemaType<TCGCEmitterOptions> = {
       nullable: true,
       description: "Set `emitter-name` to output TCGC code models for specific language's emitter.",
     },
-    ...SdkEmitterOptionsSchema.properties!,
+    ...BrandedSdkEmitterOptionsSchema.properties!,
   },
 };
 
diff --git a/packages/typespec-client-generator-core/test/namespaces.test.ts b/packages/typespec-client-generator-core/test/namespaces.test.ts
index 24b6281aa4..cb010afd4c 100644
--- a/packages/typespec-client-generator-core/test/namespaces.test.ts
+++ b/packages/typespec-client-generator-core/test/namespaces.test.ts
@@ -1,7 +1,7 @@
 import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing";
 import { ok, strictEqual } from "assert";
 import { beforeEach, describe, it } from "vitest";
-import { SdkEmitterOptions } from "../src/context.js";
+import { BrandedSdkEmitterOptionsInterface } from "../src/internal-utils.js";
 import { SdkTestRunner, createSdkContextTestHelper, createSdkTestRunner } from "./test-host.js";
 
 let runner: SdkTestRunner;
@@ -257,7 +257,7 @@ describe("no namespace flag", () => {
       `,
     );
     const sdkPackage = (
-      await createSdkContextTestHelper<SdkEmitterOptions>(runner.context.program, {
+      await createSdkContextTestHelper<BrandedSdkEmitterOptionsInterface>(runner.context.program, {
         namespace: "PetStoreRenamed",
       })
     ).sdkPackage;
@@ -278,7 +278,7 @@ describe("namespace config flag", () => {
     `);
 
     const sdkPackage = (
-      await createSdkContextTestHelper<SdkEmitterOptions>(runner.context.program, {
+      await createSdkContextTestHelper<BrandedSdkEmitterOptionsInterface>(runner.context.program, {
         namespace: "Azure.Foo",
       })
     ).sdkPackage;
@@ -319,7 +319,7 @@ describe("namespace config flag", () => {
       }
     `);
     const sdkPackage = (
-      await createSdkContextTestHelper<SdkEmitterOptions>(runner.context.program, {
+      await createSdkContextTestHelper<BrandedSdkEmitterOptionsInterface>(runner.context.program, {
         namespace: "FooRenamed",
       })
     ).sdkPackage;
@@ -360,7 +360,7 @@ describe("namespace config flag", () => {
       `,
     );
     const sdkPackage = (
-      await createSdkContextTestHelper<SdkEmitterOptions>(runner.context.program, {
+      await createSdkContextTestHelper<BrandedSdkEmitterOptionsInterface>(runner.context.program, {
         namespace: "PetStoreRenamed",
       })
     ).sdkPackage;
@@ -379,9 +379,12 @@ describe("namespace config flag", () => {
     `);
 
     const sdkPackage = (
-      await createSdkContextTestHelper<SdkEmitterOptions>(runnerWithCore.context.program, {
-        namespace: "Azure.My.Service",
-      })
+      await createSdkContextTestHelper<BrandedSdkEmitterOptionsInterface>(
+        runnerWithCore.context.program,
+        {
+          namespace: "Azure.My.Service",
+        },
+      )
     ).sdkPackage;
 
     const myNamespace = sdkPackage.namespaces.find((x) => x.name === "My");
@@ -437,7 +440,7 @@ describe("namespace config flag", () => {
     `,
     );
     const sdkPackage = (
-      await createSdkContextTestHelper<SdkEmitterOptions>(runner.context.program, {
+      await createSdkContextTestHelper<BrandedSdkEmitterOptionsInterface>(runner.context.program, {
         namespace: "PetStoreFlagRenamed",
       })
     ).sdkPackage;
@@ -479,7 +482,7 @@ describe("namespace config flag", () => {
       `,
     );
     const sdkPackage = (
-      await createSdkContextTestHelper<SdkEmitterOptions>(runner.context.program, {
+      await createSdkContextTestHelper<BrandedSdkEmitterOptionsInterface>(runner.context.program, {
         namespace: "PetStoreFlagRenamed",
       })
     ).sdkPackage;
@@ -516,7 +519,7 @@ describe("namespace config flag", () => {
     `);
 
     const sdkPackage = (
-      await createSdkContextTestHelper<SdkEmitterOptions>(runner.context.program, {
+      await createSdkContextTestHelper<BrandedSdkEmitterOptionsInterface>(runner.context.program, {
         namespace: "Azure.A",
       })
     ).sdkPackage;
@@ -555,7 +558,7 @@ it("customization with models from original namespace", async () => {
   );
 
   const sdkPackage = (
-    await createSdkContextTestHelper<SdkEmitterOptions>(runner.context.program, {
+    await createSdkContextTestHelper<BrandedSdkEmitterOptionsInterface>(runner.context.program, {
       namespace: "Renamed",
     })
   ).sdkPackage;
diff --git a/packages/typespec-client-generator-core/test/test-host.ts b/packages/typespec-client-generator-core/test/test-host.ts
index 28e4fb26d5..b33b6eb846 100644
--- a/packages/typespec-client-generator-core/test/test-host.ts
+++ b/packages/typespec-client-generator-core/test/test-host.ts
@@ -11,11 +11,12 @@ import {
 import { HttpTestLibrary } from "@typespec/http/testing";
 import { RestTestLibrary } from "@typespec/rest/testing";
 import { VersioningTestLibrary } from "@typespec/versioning/testing";
-import { CreateSdkContextOptions, SdkEmitterOptions, createSdkContext } from "../src/context.js";
+import { CreateSdkContextOptions, createSdkContext } from "../src/context.js";
 import { SdkContext, SdkHttpOperation, SdkServiceOperation } from "../src/interfaces.js";
+import { BrandedSdkEmitterOptionsInterface } from "../src/internal-utils.js";
 import { SdkTestLibrary } from "../src/testing/index.js";
 
-export interface CreateSdkTestRunnerOptions extends SdkEmitterOptions {
+export interface CreateSdkTestRunnerOptions extends BrandedSdkEmitterOptionsInterface {
   emitterName?: string;
   librariesToAdd?: TypeSpecTestLibrary[];
   autoImports?: string[];
diff --git a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/emitter.md b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/emitter.md
index bad60a979b..bc572fd567 100644
--- a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/emitter.md
+++ b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/emitter.md
@@ -47,26 +47,26 @@ When set to `true`, the emitter will generate low-level protocol methods for eac
 
 When set to `true`, the emitter will generate low-level protocol methods for each service operation if `@convenientAPI` is not set for an operation. Default value is `true`.
 
-### `examples-dir`
+### `api-version`
 
 **Type:** `string`
 
-Specifies the directory where the emitter will look for example files. If the flag isn’t set, the emitter defaults to using an `examples` directory located at the project root.
+Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`.
 
-### `namespace`
+### `license`
 
-**Type:** `string`
+**Type:** `object`
 
-Specifies the namespace you want to override for namespaces set in the spec. With this config, all namespace for the spec types will default to it.
+License information for the generated client code.
 
-### `api-version`
+### `examples-dir`
 
 **Type:** `string`
 
-Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`.
+Specifies the directory where the emitter will look for example files. If the flag isn’t set, the emitter defaults to using an `examples` directory located at the project root.
 
-### `license`
+### `namespace`
 
-**Type:** `object`
+**Type:** `string`
 
-License information for the generated client code.
+Specifies the namespace you want to override for namespaces set in the spec. With this config, all namespace for the spec types will default to it.


Reviews:
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T21:36:36Z
user: login: tadelesh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T11:22:04Z
user: login: tadelesh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T11:22:57Z
user: login: weidongxu-microsoft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T00:01:09Z
user: login: tadelesh
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T02:27:08Z
user: login: weidongxu-microsoft
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T02:03:31Z
user: login: tadelesh
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T02:13:27Z

Review comments:
diff_hunk: @@ -38,12 +38,11 @@ export interface TCGCEmitterOptions extends SdkEmitterOptions {
   "emitter-name"?: string;
 }
 
-export interface SdkEmitterOptions {
+export interface UnbrandedSdkEmitterOptions {
path: packages/typespec-client-generator-core/src/context.ts
user: login: timotheeguerin
body: will all the emitters supprot those or it will still be pick an dchoose, might be beter to just export each one separately if thats the case
created_at: 2025-03-25T21:36:35Z
author_association: MEMBER
diff_hunk: @@ -38,12 +38,11 @@ export interface TCGCEmitterOptions extends SdkEmitterOptions {
   "emitter-name"?: string;
 }
 
-export interface SdkEmitterOptions {
+export interface UnbrandedSdkEmitterOptions {
   "generate-protocol-methods"?: boolean;
   "generate-convenience-methods"?: boolean;
   "api-version"?: string;
   "examples-dir"?: string;
path: packages/typespec-client-generator-core/src/context.ts
user: login: tadelesh
body: `examples-dir` should be azure only.
created_at: 2025-03-26T11:21:00Z
author_association: MEMBER
diff_hunk: @@ -38,12 +38,11 @@ export interface TCGCEmitterOptions extends SdkEmitterOptions {
   "emitter-name"?: string;
 }
 
-export interface SdkEmitterOptions {
+export interface UnbrandedSdkEmitterOptions {
path: packages/typespec-client-generator-core/src/context.ts
user: login: tadelesh
body: i agree. e.g., java does not want any flag for unbranded.
created_at: 2025-03-26T11:22:57Z
author_association: MEMBER
diff_hunk: @@ -38,12 +38,11 @@ export interface TCGCEmitterOptions extends SdkEmitterOptions {
   "emitter-name"?: string;
 }
 
-export interface SdkEmitterOptions {
+export interface UnbrandedSdkEmitterOptions {
   "generate-protocol-methods"?: boolean;
   "generate-convenience-methods"?: boolean;
   "api-version"?: string;
   "examples-dir"?: string;
path: packages/typespec-client-generator-core/src/context.ts
user: login: weidongxu-microsoft
body: For Java, currently we only expose `license` emitter option. What about .NET? Do they need to expose `generate-protocol-methods` `generate-convenience-methods`?

For input from Srikanta as well.
created_at: 2025-03-27T00:01:09Z
author_association: MEMBER
diff_hunk: @@ -38,12 +38,11 @@ export interface TCGCEmitterOptions extends SdkEmitterOptions {
   "emitter-name"?: string;
 }
 
-export interface SdkEmitterOptions {
+export interface UnbrandedSdkEmitterOptions {
   "generate-protocol-methods"?: boolean;
   "generate-convenience-methods"?: boolean;
   "api-version"?: string;
   "examples-dir"?: string;
path: packages/typespec-client-generator-core/src/context.ts
user: login: tadelesh
body: we are going to export each option separately which could give emitter enough flexibility.
created_at: 2025-03-27T02:27:08Z
author_association: MEMBER

PR comments:
user: login: azure-sdk
created_at: 2025-03-25T21:31:00Z
body: <!--chronus-github-change-commenter-->
All changed packages have been documented.
 - :white_check_mark: `@azure-tools/typespec-client-generator-core`

<details>
<summary>Show changes</summary>

### `@azure-tools/typespec-client-generator-core` - _breaking_ [✏️](https://github.com/Azure/typespec-azure/edit/tcgc/emitterOptionsSplit/.chronus/changes/tcgc-emitterOptionsSplit-2025-2-25-17-26-43.md?pr=/Azure/typespec-azure/pull/2447)
> Split emitter options into `UnbrandedSdkEmitterOptions` and `BrandedSdkEmitterOptions`. Each flag will be exported individually, so emitters can choose which flags to support
</details>

author_association: COLLABORATOR
user: login: azure-sdk
created_at: 2025-03-25T21:44:05Z
body: <!-- _TSP_TRYIT_COMMENT_ -->
You can try these changes here

| [🛝 Playground]( https://cadlplayground.z22.web.core.windows.net/typespec-azure/prs/2447/)|[🌐 Website](https://tspwebsitepr.z22.web.core.windows.net/typespec-azure/prs/2447/) |
| ---|--- |
author_association: COLLABORATOR
user: login: JoshLove-msft
created_at: 2025-03-26T19:14:54Z
body: > We keep branded with the original name of SdkEmitterOptions to avoid any deprecations / breaking

Can't we still make breaking changes at this point?
author_association: MEMBER
user: login: iscai-msft
created_at: 2025-03-27T15:36:56Z
body: > > We keep branded with the original name of SdkEmitterOptions to avoid any deprecations / breaking
> 
> Can't we still make breaking changes at this point?

We can, but I'm just trying to avoid it. If you feel strongly though, I'll go ahead and break the emitter options name. I'm going to actually work on exporting them each individually, which would also be a breaking change, so we might as well make everything how we want it to be
author_association: CONTRIBUTOR