From 3f730a384c2eea0e0498a6e6daadb09fc9a694f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 01:18:59 +0000 Subject: [PATCH 01/12] feat(typespec-autorest): add skip-example-copying emitter option When enabled, the emitter will not copy example files to the output directory. Instead, it will reference the source example files using relative file paths from the output location. Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/5966f0f4-258b-41b9-9753-b3b16cb262f3 Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- packages/typespec-autorest/README.md | 6 ++ packages/typespec-autorest/src/emit.ts | 3 +- packages/typespec-autorest/src/lib.ts | 14 +++++ packages/typespec-autorest/src/openapi.ts | 23 ++++++- .../test/x-ms-examples.test.ts | 61 +++++++++++++++++++ .../typespec-autorest/reference/emitter.md | 6 ++ 6 files changed, 111 insertions(+), 2 deletions(-) diff --git a/packages/typespec-autorest/README.md b/packages/typespec-autorest/README.md index 683203c87d..ac69b948c1 100644 --- a/packages/typespec-autorest/README.md +++ b/packages/typespec-autorest/README.md @@ -170,6 +170,12 @@ Strategy for applying XML serialization metadata to schemas. Determines whether output should be split into multiple files. The only supported option for splitting is "legacy-feature-files", which uses the typespec-azure-resource-manager `@feature` decorators to split into output files based on feature. +### `skip-example-copying` + +**Type:** `boolean` + +When enabled, the emitter will not copy example files to the output directory. Instead, it will reference the source example files using relative file paths. + ## Decorators ### Autorest diff --git a/packages/typespec-autorest/src/emit.ts b/packages/typespec-autorest/src/emit.ts index 9d5c39a71a..cd29749d33 100644 --- a/packages/typespec-autorest/src/emit.ts +++ b/packages/typespec-autorest/src/emit.ts @@ -115,6 +115,7 @@ export function resolveAutorestOptions( emitCommonTypesSchema: resolvedOptions["emit-common-types-schema"], xmlStrategy: resolvedOptions["xml-strategy"], outputSplitting: resolvedOptions["output-splitting"], + skipExampleCopying: resolvedOptions["skip-example-copying"], }; } @@ -322,7 +323,7 @@ async function emitOutput( }); // Copy examples to the output directory - if (result.operationExamples.length > 0) { + if (result.operationExamples.length > 0 && !options.skipExampleCopying) { const examplesPath = resolvePath(getDirectoryPath(result.outputFile), "examples"); await program.host.mkdirp(examplesPath); for (const { examples } of result.operationExamples) { diff --git a/packages/typespec-autorest/src/lib.ts b/packages/typespec-autorest/src/lib.ts index de1af15318..790b9f1cd2 100644 --- a/packages/typespec-autorest/src/lib.ts +++ b/packages/typespec-autorest/src/lib.ts @@ -118,6 +118,13 @@ export interface AutorestEmitterOptions { * which uses the typespec-azure-resource-manager `@feature` decorators to split into output files based on feature. */ "output-splitting"?: "legacy-feature-files"; + + /** + * When enabled, the emitter will not copy example files to the output directory. + * Instead, it will reference the source example files using relative file paths. + * @default false + */ + "skip-example-copying"?: boolean; } const EmitterOptionsSchema: JSONSchemaType = { @@ -254,6 +261,13 @@ const EmitterOptionsSchema: JSONSchemaType = { description: 'Determines whether output should be split into multiple files. The only supported option for splitting is "legacy-feature-files", which uses the typespec-azure-resource-manager `@feature` decorators to split into output files based on feature.', }, + "skip-example-copying": { + type: "boolean", + nullable: true, + default: false, + description: + "When enabled, the emitter will not copy example files to the output directory. Instead, it will reference the source example files using relative file paths.", + }, }, required: [], }; diff --git a/packages/typespec-autorest/src/openapi.ts b/packages/typespec-autorest/src/openapi.ts index 7db21052d0..5cb716b433 100644 --- a/packages/typespec-autorest/src/openapi.ts +++ b/packages/typespec-autorest/src/openapi.ts @@ -253,6 +253,13 @@ export interface AutorestDocumentEmitterOptions { * Determines whether output should be split into multiple files. The only supported option for splitting is "legacy-feature-files", */ readonly outputSplitting?: "legacy-feature-files"; + + /** + * When enabled, example files will not be copied to the output directory. + * Instead, the source example files will be referenced using relative file paths. + * @default false + */ + readonly skipExampleCopying?: boolean; } type HttpParameterProperties = Extract< @@ -316,6 +323,12 @@ export async function getOpenAPIForService( const operationIdsWithExample = new Set(); + // Compute the example directory for resolving source example paths + const examplesBaseDir = options.examplesDirectory ?? resolvePath(program.projectRoot, "examples"); + const exampleDir = context.version + ? resolvePath(examplesBaseDir, context.version) + : resolvePath(examplesBaseDir); + const [exampleMap, diagnostics] = await loadExamples(program, options, context.version); program.reportDiagnostics(diagnostics); @@ -609,7 +622,15 @@ export async function getOpenAPIForService( operationIdsWithExample.add(currentEndpoint.operationId); currentEndpoint["x-ms-examples"] = currentEndpoint["x-ms-examples"] || {}; for (const [title, example] of Object.entries(autoExamples)) { - currentEndpoint["x-ms-examples"][title] = { $ref: `./examples/${example.relativePath}` }; + let ref: string; + if (options.skipExampleCopying) { + const sourceExamplePath = resolvePath(exampleDir, example.relativePath); + const outputDir = getDirectoryPath(context.outputFile); + ref = getRelativePathFromDirectory(outputDir, sourceExamplePath, false); + } else { + ref = `./examples/${example.relativePath}`; + } + currentEndpoint["x-ms-examples"][title] = { $ref: ref }; } } diff --git a/packages/typespec-autorest/test/x-ms-examples.test.ts b/packages/typespec-autorest/test/x-ms-examples.test.ts index a578b337ce..63c0c42f4c 100644 --- a/packages/typespec-autorest/test/x-ms-examples.test.ts +++ b/packages/typespec-autorest/test/x-ms-examples.test.ts @@ -204,4 +204,65 @@ describe("explicit example", () => { code: "@azure-tools/typespec-autorest/duplicate-example-file", }); }); + + describe("with skip-example-copying", () => { + it("references source example with relative path and does not copy", async () => { + addExampleFile("./examples/getPet.json", { operationId: "Pets_get", title: "Get a pet" }); + + const openapi = await compileOpenAPI(`@operationId("Pets_get") op read(): void;`, { + tester, + options: { "skip-example-copying": true }, + }); + + deepStrictEqual(openapi.paths["/"]?.get?.["x-ms-examples"], { + "Get a pet": { + $ref: "../examples/getPet.json", + }, + }); + expect(tester.fs.fs.has(resolveVirtualPath("./tsp-output/examples/getPet.json"))).toBe(false); + }); + + it("references nested source example with relative path and does not copy", async () => { + addExampleFile("./examples/pets/getPet.json", { + operationId: "Pets_get", + title: "Get a pet", + }); + + const openapi = await compileOpenAPI(`@operationId("Pets_get") op read(): void;`, { + tester, + options: { "skip-example-copying": true }, + }); + + deepStrictEqual(openapi.paths["/"]?.get?.["x-ms-examples"], { + "Get a pet": { + $ref: "../examples/pets/getPet.json", + }, + }); + expect(tester.fs.fs.has(resolveVirtualPath("./tsp-output/examples/pets/getPet.json"))).toBe( + false, + ); + }); + + it("references source example from custom examples-dir", async () => { + addExampleFile("./my-examples/getPet.json", { + operationId: "Pets_get", + title: "Get a pet", + }); + + const openapi = await compileOpenAPI(`@operationId("Pets_get") op read(): void;`, { + tester, + options: { + "skip-example-copying": true, + "examples-dir": resolveVirtualPath("./my-examples"), + }, + }); + + deepStrictEqual(openapi.paths["/"]?.get?.["x-ms-examples"], { + "Get a pet": { + $ref: "../my-examples/getPet.json", + }, + }); + expect(tester.fs.fs.has(resolveVirtualPath("./tsp-output/examples/getPet.json"))).toBe(false); + }); + }); }); diff --git a/website/src/content/docs/docs/emitters/typespec-autorest/reference/emitter.md b/website/src/content/docs/docs/emitters/typespec-autorest/reference/emitter.md index 3122bcfbc5..2df0df13c8 100644 --- a/website/src/content/docs/docs/emitters/typespec-autorest/reference/emitter.md +++ b/website/src/content/docs/docs/emitters/typespec-autorest/reference/emitter.md @@ -163,3 +163,9 @@ Strategy for applying XML serialization metadata to schemas. **Type:** `"legacy-feature-files"` Determines whether output should be split into multiple files. The only supported option for splitting is "legacy-feature-files", which uses the typespec-azure-resource-manager `@feature` decorators to split into output files based on feature. + +### `skip-example-copying` + +**Type:** `boolean` + +When enabled, the emitter will not copy example files to the output directory. Instead, it will reference the source example files using relative file paths. From 9e3df9bbd3966ee222e06e3f668f1ec7a6ba7563 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:34:16 +0000 Subject: [PATCH 02/12] add changeset and versioned spec test for skip-example-copying Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/774e6722-332b-4c31-acf6-e8363ef4a705 Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- ...kip-example-copying-2026-04-03-05-20-45.md | 7 ++ .../test/x-ms-examples.test.ts | 67 ++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 .chronus/changes/skip-example-copying-2026-04-03-05-20-45.md diff --git a/.chronus/changes/skip-example-copying-2026-04-03-05-20-45.md b/.chronus/changes/skip-example-copying-2026-04-03-05-20-45.md new file mode 100644 index 0000000000..71c06c5aac --- /dev/null +++ b/.chronus/changes/skip-example-copying-2026-04-03-05-20-45.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-autorest" +--- + +Added `skip-example-copying` emitter option. When enabled, example files are not copied to the output directory and `x-ms-examples` `$ref` values point directly to the source example files via relative paths. diff --git a/packages/typespec-autorest/test/x-ms-examples.test.ts b/packages/typespec-autorest/test/x-ms-examples.test.ts index 63c0c42f4c..ba660e2942 100644 --- a/packages/typespec-autorest/test/x-ms-examples.test.ts +++ b/packages/typespec-autorest/test/x-ms-examples.test.ts @@ -1,12 +1,18 @@ import { EmitterTesterInstance, + expectDiagnosticEmpty, expectDiagnostics, resolveVirtualPath, TestEmitterCompileResult, } from "@typespec/compiler/testing"; import { deepStrictEqual } from "assert"; import { beforeEach, describe, expect, it } from "vitest"; -import { compileOpenAPI, ignoreUseStandardOps, Tester } from "./test-host.js"; +import { + compileOpenAPI, + ignoreDiagnostics, + ignoreUseStandardOps, + Tester, +} from "./test-host.js"; let tester: EmitterTesterInstance; @@ -264,5 +270,64 @@ describe("explicit example", () => { }); expect(tester.fs.fs.has(resolveVirtualPath("./tsp-output/examples/getPet.json"))).toBe(false); }); + + it("references source example with relative path in versioned spec and does not copy", async () => { + tester.fs.add( + "./examples/v1/getPet.json", + JSON.stringify({ operationId: "Pets_get", title: "Get a pet" }), + ); + tester.fs.add( + "./examples/v2/getPet.json", + JSON.stringify({ operationId: "Pets_get", title: "Get a pet" }), + ); + + const emitterOutputDir = resolveVirtualPath("./tsp-output"); + const [{ outputs }, diagnostics] = await tester.compileAndDiagnose( + ` +@versioned(Versions) +@service(#{title: "Pet Service"}) +namespace PetService; +enum Versions {v1, v2} + +@operationId("Pets_get") +op read(): void; +`, + { + compilerOptions: { + options: { + "@azure-tools/typespec-autorest": { + "emitter-output-dir": emitterOutputDir, + "skip-example-copying": true, + }, + }, + }, + }, + ); + expectDiagnosticEmpty( + ignoreDiagnostics(diagnostics, ["@typespec/http/no-service-found"]), + ); + + const v1Doc = JSON.parse(outputs["stable/v1/openapi.json"]); + const v2Doc = JSON.parse(outputs["stable/v2/openapi.json"]); + + deepStrictEqual(v1Doc.paths["/"]?.get?.["x-ms-examples"], { + "Get a pet": { + $ref: "../../../examples/v1/getPet.json", + }, + }); + deepStrictEqual(v2Doc.paths["/"]?.get?.["x-ms-examples"], { + "Get a pet": { + $ref: "../../../examples/v2/getPet.json", + }, + }); + + // Verify examples were NOT copied to the output directory + expect( + tester.fs.fs.has(resolveVirtualPath("./tsp-output/stable/v1/examples/getPet.json")), + ).toBe(false); + expect( + tester.fs.fs.has(resolveVirtualPath("./tsp-output/stable/v2/examples/getPet.json")), + ).toBe(false); + }); }); }); From b520b3ff2fa55bda21f65a344eba4db8013a30e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:47:18 +0000 Subject: [PATCH 03/12] fix: apply formatting to test file Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/a5a493be-c2a6-4f3a-afc1-182c25389195 Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- packages/typespec-autorest/test/x-ms-examples.test.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/typespec-autorest/test/x-ms-examples.test.ts b/packages/typespec-autorest/test/x-ms-examples.test.ts index ba660e2942..d94209044f 100644 --- a/packages/typespec-autorest/test/x-ms-examples.test.ts +++ b/packages/typespec-autorest/test/x-ms-examples.test.ts @@ -7,12 +7,7 @@ import { } from "@typespec/compiler/testing"; import { deepStrictEqual } from "assert"; import { beforeEach, describe, expect, it } from "vitest"; -import { - compileOpenAPI, - ignoreDiagnostics, - ignoreUseStandardOps, - Tester, -} from "./test-host.js"; +import { compileOpenAPI, ignoreDiagnostics, ignoreUseStandardOps, Tester } from "./test-host.js"; let tester: EmitterTesterInstance; @@ -303,9 +298,7 @@ op read(): void; }, }, ); - expectDiagnosticEmpty( - ignoreDiagnostics(diagnostics, ["@typespec/http/no-service-found"]), - ); + expectDiagnosticEmpty(ignoreDiagnostics(diagnostics, ["@typespec/http/no-service-found"])); const v1Doc = JSON.parse(outputs["stable/v1/openapi.json"]); const v2Doc = JSON.parse(outputs["stable/v2/openapi.json"]); From 62c9e93b9071e4c0bc7edc184bf4474c8f99993d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:06:16 +0000 Subject: [PATCH 04/12] update init feeds and x-ms-examples docs to use skip-example-copying Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/0e356775-70e8-49d6-92a6-ec924dcbba65 Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- .../__snapshots__/azure-arm/tspconfig.yaml | 1 + .../azure-arm_stand_alone/tspconfig.yaml | 1 + .../__snapshots__/azure-core/tspconfig.yaml | 5 +++-- .../azure-core_stand_alone/tspconfig.yaml | 1 + eng/feeds/arm-canonical/tspconfig.yaml | 1 + eng/feeds/arm/tspconfig.yaml | 1 + eng/feeds/arm/tspconfig_stand_alone.yaml | 1 + eng/feeds/data-plane/tspconfig.yaml | 1 + .../data-plane/tspconfig_stand_alone.yaml | 1 + .../migrate-swagger/faq/x-ms-examples.mdx | 22 +++++++++++++++---- 10 files changed, 29 insertions(+), 6 deletions(-) diff --git a/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml b/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml index 81bd7acded..206cc48ff3 100644 --- a/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml @@ -6,6 +6,7 @@ options: emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/widget.json" arm-types-dir: "{project-root}/../../../../common-types/resource-management" + skip-example-copying: true "@azure-tools/typespec-csharp": emitter-output-dir: "{output-dir}/{service-dir}/Azure.ResourceManager.Contoso" clear-output-folder: true diff --git a/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml b/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml index 24a4c4a5c9..ca830d0135 100644 --- a/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml @@ -4,6 +4,7 @@ options: "@azure-tools/typespec-autorest": use-read-only-status-schema: true output-file: "resource-manager/{service-name}/{version-status}/{version}/openapi.json" + skip-example-copying: true linter: extends: - "@azure-tools/typespec-azure-rulesets/resource-manager" diff --git a/eng/feeds/__snapshots__/azure-core/tspconfig.yaml b/eng/feeds/__snapshots__/azure-core/tspconfig.yaml index 32af772b8a..e508de2bfd 100644 --- a/eng/feeds/__snapshots__/azure-core/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-core/tspconfig.yaml @@ -7,8 +7,9 @@ emit: - "@azure-tools/typespec-autorest" options: "@azure-tools/typespec-autorest": - emitter-output-dir: "{project-root}/.." - output-file: "data-plane/{service-name}/{version-status}/{version}/openapi.json" + emitter-output-dir: "{project-root}" + output-file: "{emitter-output-dir}/{version-status}/{version}/openapi.json" + skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/azure-contoso" namespace: "azure.contoso" diff --git a/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml b/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml index 17ce84a8cc..d782695dd2 100644 --- a/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml @@ -8,6 +8,7 @@ emit: options: "@azure-tools/typespec-autorest": output-file: "data-plane/{service-name}/{version-status}/{version}/openapi.json" + skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/azure-contoso" namespace: azure.contoso diff --git a/eng/feeds/arm-canonical/tspconfig.yaml b/eng/feeds/arm-canonical/tspconfig.yaml index 1e5c9120ac..374e82d49d 100644 --- a/eng/feeds/arm-canonical/tspconfig.yaml +++ b/eng/feeds/arm-canonical/tspconfig.yaml @@ -7,6 +7,7 @@ options: emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/widget.json" arm-types-dir: "{project-root}/../../../../common-types/resource-management" + skip-example-copying: true "@azure-tools/typespec-autorest-canonical": emitter-output-dir: "{project-root}/.." output-file: "{emitter-output-dir}/canonical/openapi.json" diff --git a/eng/feeds/arm/tspconfig.yaml b/eng/feeds/arm/tspconfig.yaml index d94354561d..ac2d3cfb1e 100644 --- a/eng/feeds/arm/tspconfig.yaml +++ b/eng/feeds/arm/tspconfig.yaml @@ -6,6 +6,7 @@ options: emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/widget.json" arm-types-dir: "{project-root}/../../../../common-types/resource-management" + skip-example-copying: true "@azure-tools/typespec-csharp": emitter-output-dir: "{output-dir}/{service-dir}/Azure.ResourceManager.{{parameters.ServiceNamespace}}" clear-output-folder: true diff --git a/eng/feeds/arm/tspconfig_stand_alone.yaml b/eng/feeds/arm/tspconfig_stand_alone.yaml index 24a4c4a5c9..ca830d0135 100644 --- a/eng/feeds/arm/tspconfig_stand_alone.yaml +++ b/eng/feeds/arm/tspconfig_stand_alone.yaml @@ -4,6 +4,7 @@ options: "@azure-tools/typespec-autorest": use-read-only-status-schema: true output-file: "resource-manager/{service-name}/{version-status}/{version}/openapi.json" + skip-example-copying: true linter: extends: - "@azure-tools/typespec-azure-rulesets/resource-manager" diff --git a/eng/feeds/data-plane/tspconfig.yaml b/eng/feeds/data-plane/tspconfig.yaml index 4bb38b8e88..6695cf30ba 100644 --- a/eng/feeds/data-plane/tspconfig.yaml +++ b/eng/feeds/data-plane/tspconfig.yaml @@ -9,6 +9,7 @@ options: "@azure-tools/typespec-autorest": emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/openapi.json" + skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/{{#normalizePackageName}}{{parameters.ServiceNamespace}}{{/normalizePackageName}}" namespace: "{{#toLowerCase}}{{parameters.ServiceNamespace}}{{/toLowerCase}}" diff --git a/eng/feeds/data-plane/tspconfig_stand_alone.yaml b/eng/feeds/data-plane/tspconfig_stand_alone.yaml index 09e2c6fa22..eb10e9aa9f 100644 --- a/eng/feeds/data-plane/tspconfig_stand_alone.yaml +++ b/eng/feeds/data-plane/tspconfig_stand_alone.yaml @@ -8,6 +8,7 @@ emit: options: "@azure-tools/typespec-autorest": output-file: "data-plane/{service-name}/{version-status}/{version}/openapi.json" + skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/{{#normalizePackageName}}{{parameters.ServiceNamespace}}{{/normalizePackageName}}" namespace: {{#toLowerCase}}{{parameters.ServiceNamespace}}{{/toLowerCase}} diff --git a/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx b/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx index 5f16248da5..ab1952220e 100644 --- a/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx +++ b/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx @@ -5,21 +5,34 @@ title: x-ms-examples example files import { FileTree } from "@astrojs/starlight/components"; The `x-ms-examples` is automatically populated in the generated OpenAPI 2.0 when using the `typespec-autorest` emitter. -The examples must be placed in the `examples-directory` (default to `{project-root}/examples`) and have the `operationdId` property. +The examples must be placed in the `examples-directory` (default to `{project-root}/examples`) and have the `operationId` property. :::caution Do not use `@extension("x-ms-examples", "")`. ::: +## Recommended configuration + +We recommend enabling the `skip-example-copying` option in your `tspconfig.yaml`. This prevents example files from being copied into the output directory and instead generates `x-ms-examples` `$ref` values that point directly to the source example files using relative paths. + +```yaml +options: + "@azure-tools/typespec-autorest": + skip-example-copying: true +``` + +With this setting, example files remain in the source `examples` directory and are referenced in the generated OpenAPI spec via relative paths from the output file location. + ## Example structure -Example below assume `example-directory` is `{project-root}/examples`. +Examples below assume `examples-dir` is `{project-root}/examples` (the default). - Single version structure {/* prettier-ignore */} - main.tsp +- tspconfig.yaml - examples/ - example1.json - example2.json @@ -30,6 +43,7 @@ Example below assume `example-directory` is `{project-root}/examples`. {/* prettier-ignore */} - main.tsp +- tspconfig.yaml - examples/ - 2021-01-01/ - example1.json @@ -54,6 +68,6 @@ Note, latest OAV tool should automatically generate the following. However, if y - include `title` field and make sure it is descriptive and unique for each operation. - include `operationId`. This is used to match with declared operations in TypeSpec and correctly output in swagger. -:::caution -The examples are now in the examples directory relative to the output openapi.json. You must now copy them to the examples directory in the project root. The typespec-autorest emitter will then copy them back to the correct location when generating the OpenAPI 2.0. +:::note +When using `skip-example-copying: true`, the generated examples from `oav` will be in the output directory next to `openapi.json`. You should move them to the source `examples` directory in your project root so they are referenced correctly on subsequent compilations. ::: From 7fc6ab45e2efd8decc0175bba50faf0984f509fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:07:42 +0000 Subject: [PATCH 05/12] fix: use consistent examples-dir naming in doc Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/0e356775-70e8-49d6-92a6-ec924dcbba65 Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- .../src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx b/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx index ab1952220e..025fef9d6a 100644 --- a/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx +++ b/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx @@ -5,7 +5,7 @@ title: x-ms-examples example files import { FileTree } from "@astrojs/starlight/components"; The `x-ms-examples` is automatically populated in the generated OpenAPI 2.0 when using the `typespec-autorest` emitter. -The examples must be placed in the `examples-directory` (default to `{project-root}/examples`) and have the `operationId` property. +The examples must be placed in the `examples-dir` directory (default `{project-root}/examples`) and have the `operationId` property. :::caution Do not use `@extension("x-ms-examples", "")`. From d32fb083003f574953bfc521fa1bae1b8be782a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:20:10 +0000 Subject: [PATCH 06/12] add examples-dir setting to tspconfig.yaml files in feeds and rebuild snapshots Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/6bb7e57d-bb20-4569-9e32-5ca3f5245044 Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- eng/feeds/__snapshots__/azure-arm/tspconfig.yaml | 1 + eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml | 1 + eng/feeds/__snapshots__/azure-core/tspconfig.yaml | 1 + eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml | 1 + eng/feeds/arm-canonical/tspconfig.yaml | 1 + eng/feeds/arm/tspconfig.yaml | 1 + eng/feeds/arm/tspconfig_stand_alone.yaml | 1 + eng/feeds/data-plane/tspconfig.yaml | 1 + eng/feeds/data-plane/tspconfig_stand_alone.yaml | 1 + 9 files changed, 9 insertions(+) diff --git a/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml b/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml index 206cc48ff3..cde1159da5 100644 --- a/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml @@ -6,6 +6,7 @@ options: emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/widget.json" arm-types-dir: "{project-root}/../../../../common-types/resource-management" + examples-dir: "{project-root}/examples" skip-example-copying: true "@azure-tools/typespec-csharp": emitter-output-dir: "{output-dir}/{service-dir}/Azure.ResourceManager.Contoso" diff --git a/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml b/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml index ca830d0135..d1600fff5e 100644 --- a/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml @@ -4,6 +4,7 @@ options: "@azure-tools/typespec-autorest": use-read-only-status-schema: true output-file: "resource-manager/{service-name}/{version-status}/{version}/openapi.json" + examples-dir: "{project-root}/examples" skip-example-copying: true linter: extends: diff --git a/eng/feeds/__snapshots__/azure-core/tspconfig.yaml b/eng/feeds/__snapshots__/azure-core/tspconfig.yaml index e508de2bfd..14a2871ed3 100644 --- a/eng/feeds/__snapshots__/azure-core/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-core/tspconfig.yaml @@ -9,6 +9,7 @@ options: "@azure-tools/typespec-autorest": emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/openapi.json" + examples-dir: "{project-root}/examples" skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/azure-contoso" diff --git a/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml b/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml index d782695dd2..40813d7b38 100644 --- a/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml @@ -8,6 +8,7 @@ emit: options: "@azure-tools/typespec-autorest": output-file: "data-plane/{service-name}/{version-status}/{version}/openapi.json" + examples-dir: "{project-root}/examples" skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/azure-contoso" diff --git a/eng/feeds/arm-canonical/tspconfig.yaml b/eng/feeds/arm-canonical/tspconfig.yaml index 374e82d49d..f1866eefa1 100644 --- a/eng/feeds/arm-canonical/tspconfig.yaml +++ b/eng/feeds/arm-canonical/tspconfig.yaml @@ -7,6 +7,7 @@ options: emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/widget.json" arm-types-dir: "{project-root}/../../../../common-types/resource-management" + examples-dir: "{project-root}/examples" skip-example-copying: true "@azure-tools/typespec-autorest-canonical": emitter-output-dir: "{project-root}/.." diff --git a/eng/feeds/arm/tspconfig.yaml b/eng/feeds/arm/tspconfig.yaml index ac2d3cfb1e..6c8bbbc04b 100644 --- a/eng/feeds/arm/tspconfig.yaml +++ b/eng/feeds/arm/tspconfig.yaml @@ -6,6 +6,7 @@ options: emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/widget.json" arm-types-dir: "{project-root}/../../../../common-types/resource-management" + examples-dir: "{project-root}/examples" skip-example-copying: true "@azure-tools/typespec-csharp": emitter-output-dir: "{output-dir}/{service-dir}/Azure.ResourceManager.{{parameters.ServiceNamespace}}" diff --git a/eng/feeds/arm/tspconfig_stand_alone.yaml b/eng/feeds/arm/tspconfig_stand_alone.yaml index ca830d0135..d1600fff5e 100644 --- a/eng/feeds/arm/tspconfig_stand_alone.yaml +++ b/eng/feeds/arm/tspconfig_stand_alone.yaml @@ -4,6 +4,7 @@ options: "@azure-tools/typespec-autorest": use-read-only-status-schema: true output-file: "resource-manager/{service-name}/{version-status}/{version}/openapi.json" + examples-dir: "{project-root}/examples" skip-example-copying: true linter: extends: diff --git a/eng/feeds/data-plane/tspconfig.yaml b/eng/feeds/data-plane/tspconfig.yaml index 6695cf30ba..9df737c926 100644 --- a/eng/feeds/data-plane/tspconfig.yaml +++ b/eng/feeds/data-plane/tspconfig.yaml @@ -9,6 +9,7 @@ options: "@azure-tools/typespec-autorest": emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/openapi.json" + examples-dir: "{project-root}/examples" skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/{{#normalizePackageName}}{{parameters.ServiceNamespace}}{{/normalizePackageName}}" diff --git a/eng/feeds/data-plane/tspconfig_stand_alone.yaml b/eng/feeds/data-plane/tspconfig_stand_alone.yaml index eb10e9aa9f..7e9be532d6 100644 --- a/eng/feeds/data-plane/tspconfig_stand_alone.yaml +++ b/eng/feeds/data-plane/tspconfig_stand_alone.yaml @@ -8,6 +8,7 @@ emit: options: "@azure-tools/typespec-autorest": output-file: "data-plane/{service-name}/{version-status}/{version}/openapi.json" + examples-dir: "{project-root}/examples" skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/{{#normalizePackageName}}{{parameters.ServiceNamespace}}{{/normalizePackageName}}" From b8664fb60b50f14aebb86c2d4230f9bf11cb10ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:58:57 +0000 Subject: [PATCH 07/12] fix: remove skip-example-copying from standalone tspconfig files (not yet published to npm) Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/4aa2af3a-33fe-420d-854d-94d8c9a8345c Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml | 1 - eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml | 1 - eng/feeds/arm/tspconfig_stand_alone.yaml | 1 - eng/feeds/data-plane/tspconfig_stand_alone.yaml | 1 - 4 files changed, 4 deletions(-) diff --git a/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml b/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml index d1600fff5e..035da34ef0 100644 --- a/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-arm_stand_alone/tspconfig.yaml @@ -5,7 +5,6 @@ options: use-read-only-status-schema: true output-file: "resource-manager/{service-name}/{version-status}/{version}/openapi.json" examples-dir: "{project-root}/examples" - skip-example-copying: true linter: extends: - "@azure-tools/typespec-azure-rulesets/resource-manager" diff --git a/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml b/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml index 40813d7b38..dd4f7bb641 100644 --- a/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-core_stand_alone/tspconfig.yaml @@ -9,7 +9,6 @@ options: "@azure-tools/typespec-autorest": output-file: "data-plane/{service-name}/{version-status}/{version}/openapi.json" examples-dir: "{project-root}/examples" - skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/azure-contoso" namespace: azure.contoso diff --git a/eng/feeds/arm/tspconfig_stand_alone.yaml b/eng/feeds/arm/tspconfig_stand_alone.yaml index d1600fff5e..035da34ef0 100644 --- a/eng/feeds/arm/tspconfig_stand_alone.yaml +++ b/eng/feeds/arm/tspconfig_stand_alone.yaml @@ -5,7 +5,6 @@ options: use-read-only-status-schema: true output-file: "resource-manager/{service-name}/{version-status}/{version}/openapi.json" examples-dir: "{project-root}/examples" - skip-example-copying: true linter: extends: - "@azure-tools/typespec-azure-rulesets/resource-manager" diff --git a/eng/feeds/data-plane/tspconfig_stand_alone.yaml b/eng/feeds/data-plane/tspconfig_stand_alone.yaml index 7e9be532d6..cb7f0447c0 100644 --- a/eng/feeds/data-plane/tspconfig_stand_alone.yaml +++ b/eng/feeds/data-plane/tspconfig_stand_alone.yaml @@ -9,7 +9,6 @@ options: "@azure-tools/typespec-autorest": output-file: "data-plane/{service-name}/{version-status}/{version}/openapi.json" examples-dir: "{project-root}/examples" - skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/{{#normalizePackageName}}{{parameters.ServiceNamespace}}{{/normalizePackageName}}" namespace: {{#toLowerCase}}{{parameters.ServiceNamespace}}{{/toLowerCase}} From 7e3f1985c15e82c22ea94396c90cdb6de14d9aff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 20:21:26 +0000 Subject: [PATCH 08/12] feat: support {version-status} and {version} interpolation in examples-dir for versioned output folder structure Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/521b6462-0e02-4b6d-b8e7-fd79cb7f3c39 Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- .../__snapshots__/azure-arm/tspconfig.yaml | 2 +- .../azure-arm_stand_alone/package.json | 20 +++++----- .../__snapshots__/azure-core/tspconfig.yaml | 2 +- .../azure-core_stand_alone/package.json | 20 +++++----- eng/feeds/arm-canonical/tspconfig.yaml | 2 +- eng/feeds/arm/tspconfig.yaml | 2 +- eng/feeds/data-plane/tspconfig.yaml | 2 +- packages/typespec-autorest/src/openapi.ts | 40 ++++++++++++++++--- .../test/x-ms-examples.test.ts | 29 ++++++-------- .../migrate-swagger/faq/x-ms-examples.mdx | 35 +++++++++------- 10 files changed, 92 insertions(+), 62 deletions(-) diff --git a/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml b/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml index cde1159da5..72d0bd3ceb 100644 --- a/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-arm/tspconfig.yaml @@ -6,7 +6,7 @@ options: emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/widget.json" arm-types-dir: "{project-root}/../../../../common-types/resource-management" - examples-dir: "{project-root}/examples" + examples-dir: "{emitter-output-dir}/{version-status}/{version}/examples" skip-example-copying: true "@azure-tools/typespec-csharp": emitter-output-dir: "{output-dir}/{service-dir}/Azure.ResourceManager.Contoso" diff --git a/eng/feeds/__snapshots__/azure-arm_stand_alone/package.json b/eng/feeds/__snapshots__/azure-arm_stand_alone/package.json index 8e1ec45eae..f9198f892a 100644 --- a/eng/feeds/__snapshots__/azure-arm_stand_alone/package.json +++ b/eng/feeds/__snapshots__/azure-arm_stand_alone/package.json @@ -4,15 +4,15 @@ "type": "module", "private": true, "dependencies": { - "@typespec/compiler": "latest", - "@azure-tools/typespec-autorest": "latest", - "@azure-tools/typespec-azure-core": "latest", - "@azure-tools/typespec-azure-resource-manager": "latest", - "@azure-tools/typespec-providerhub": "latest", - "@azure-tools/typespec-azure-rulesets": "latest", - "@typespec/http": "latest", - "@typespec/openapi": "latest", - "@typespec/rest": "latest", - "@typespec/versioning": "latest" + "@typespec/compiler": "^1.11.0", + "@azure-tools/typespec-autorest": "^0.67.0", + "@azure-tools/typespec-azure-core": "^0.67.1", + "@azure-tools/typespec-azure-resource-manager": "^0.67.1", + "@azure-tools/typespec-providerhub": "^0.50.0", + "@azure-tools/typespec-azure-rulesets": "^0.67.0", + "@typespec/http": "^1.11.0", + "@typespec/openapi": "^1.11.0", + "@typespec/rest": "^0.81.0", + "@typespec/versioning": "^0.81.0" } } \ No newline at end of file diff --git a/eng/feeds/__snapshots__/azure-core/tspconfig.yaml b/eng/feeds/__snapshots__/azure-core/tspconfig.yaml index 14a2871ed3..19662c4c0b 100644 --- a/eng/feeds/__snapshots__/azure-core/tspconfig.yaml +++ b/eng/feeds/__snapshots__/azure-core/tspconfig.yaml @@ -9,7 +9,7 @@ options: "@azure-tools/typespec-autorest": emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/openapi.json" - examples-dir: "{project-root}/examples" + examples-dir: "{emitter-output-dir}/{version-status}/{version}/examples" skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/azure-contoso" diff --git a/eng/feeds/__snapshots__/azure-core_stand_alone/package.json b/eng/feeds/__snapshots__/azure-core_stand_alone/package.json index 75914e24ea..d54b5c0ecb 100644 --- a/eng/feeds/__snapshots__/azure-core_stand_alone/package.json +++ b/eng/feeds/__snapshots__/azure-core_stand_alone/package.json @@ -4,15 +4,15 @@ "type": "module", "private": true, "dependencies": { - "@typespec/compiler": "latest", - "@azure-tools/typespec-autorest": "latest", - "@azure-tools/typespec-azure-core": "latest", - "@azure-tools/typespec-azure-resource-manager": "latest", - "@azure-tools/typespec-client-generator-core": "latest", - "@azure-tools/typespec-azure-rulesets": "latest", - "@typespec/http": "latest", - "@typespec/openapi": "latest", - "@typespec/rest": "latest", - "@typespec/versioning": "latest" + "@typespec/compiler": "^1.11.0", + "@azure-tools/typespec-autorest": "^0.67.0", + "@azure-tools/typespec-azure-core": "^0.67.1", + "@azure-tools/typespec-azure-resource-manager": "^0.67.1", + "@azure-tools/typespec-client-generator-core": "^0.67.4", + "@azure-tools/typespec-azure-rulesets": "^0.67.0", + "@typespec/http": "^1.11.0", + "@typespec/openapi": "^1.11.0", + "@typespec/rest": "^0.81.0", + "@typespec/versioning": "^0.81.0" } } \ No newline at end of file diff --git a/eng/feeds/arm-canonical/tspconfig.yaml b/eng/feeds/arm-canonical/tspconfig.yaml index f1866eefa1..d353cf22f2 100644 --- a/eng/feeds/arm-canonical/tspconfig.yaml +++ b/eng/feeds/arm-canonical/tspconfig.yaml @@ -7,7 +7,7 @@ options: emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/widget.json" arm-types-dir: "{project-root}/../../../../common-types/resource-management" - examples-dir: "{project-root}/examples" + examples-dir: "{emitter-output-dir}/{version-status}/{version}/examples" skip-example-copying: true "@azure-tools/typespec-autorest-canonical": emitter-output-dir: "{project-root}/.." diff --git a/eng/feeds/arm/tspconfig.yaml b/eng/feeds/arm/tspconfig.yaml index 6c8bbbc04b..9e2ee3331e 100644 --- a/eng/feeds/arm/tspconfig.yaml +++ b/eng/feeds/arm/tspconfig.yaml @@ -6,7 +6,7 @@ options: emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/widget.json" arm-types-dir: "{project-root}/../../../../common-types/resource-management" - examples-dir: "{project-root}/examples" + examples-dir: "{emitter-output-dir}/{version-status}/{version}/examples" skip-example-copying: true "@azure-tools/typespec-csharp": emitter-output-dir: "{output-dir}/{service-dir}/Azure.ResourceManager.{{parameters.ServiceNamespace}}" diff --git a/eng/feeds/data-plane/tspconfig.yaml b/eng/feeds/data-plane/tspconfig.yaml index 9df737c926..3c5389219a 100644 --- a/eng/feeds/data-plane/tspconfig.yaml +++ b/eng/feeds/data-plane/tspconfig.yaml @@ -9,7 +9,7 @@ options: "@azure-tools/typespec-autorest": emitter-output-dir: "{project-root}" output-file: "{emitter-output-dir}/{version-status}/{version}/openapi.json" - examples-dir: "{project-root}/examples" + examples-dir: "{emitter-output-dir}/{version-status}/{version}/examples" skip-example-copying: true "@azure-tools/typespec-python": emitter-output-dir: "{output-dir}/{service-dir}/{{#normalizePackageName}}{{parameters.ServiceNamespace}}{{/normalizePackageName}}" diff --git a/packages/typespec-autorest/src/openapi.ts b/packages/typespec-autorest/src/openapi.ts index b7d4b541b5..4e75d1aee3 100644 --- a/packages/typespec-autorest/src/openapi.ts +++ b/packages/typespec-autorest/src/openapi.ts @@ -325,10 +325,11 @@ export async function getOpenAPIForService( const operationIdsWithExample = new Set(); // Compute the example directory for resolving source example paths - const examplesBaseDir = options.examplesDirectory ?? resolvePath(program.projectRoot, "examples"); - const exampleDir = context.version - ? resolvePath(examplesBaseDir, context.version) - : resolvePath(examplesBaseDir); + const exampleDir = resolveExampleDir( + options.examplesDirectory, + program.projectRoot, + context.version, + ); const [exampleMap, diagnostics] = await loadExamples(program, options, context.version); program.reportDiagnostics(diagnostics); @@ -2820,6 +2821,34 @@ export function sortOpenAPIDocument(doc: OpenAPI2Document): OpenAPI2Document { return sorted; } +/** + * Resolves the example directory path, supporting `{version}` and `{version-status}` interpolation + * variables in the `examples-dir` option. + * + * When the examples-dir contains `{version}` or `{version-status}`, these are interpolated + * with the actual version values and the resulting path is used directly. + * Otherwise, the version is appended as a subdirectory (legacy behavior). + */ +function resolveExampleDir( + examplesDirectory: string | undefined, + projectRoot: string, + version: string | undefined, +): string { + const rawDir = examplesDirectory ?? resolvePath(projectRoot, "examples"); + const hasVersionInterpolation = + rawDir.includes("{version}") || rawDir.includes("{version-status}"); + + if (hasVersionInterpolation) { + const versionStatus = version && (version.includes("preview") ? "preview" : "stable"); + return interpolatePath(rawDir, { + "version-status": versionStatus, + version: version, + }); + } + + return version ? resolvePath(rawDir, version) : rawDir; +} + async function checkExamplesDirExists(host: CompilerHost, dir: string) { try { return (await host.stat(dir)).isDirectory(); @@ -2862,8 +2891,7 @@ async function loadExamples( ): Promise<[Map>, readonly Diagnostic[]]> { const host = program.host; const diagnostics = createDiagnosticCollector(); - const examplesBaseDir = options.examplesDirectory ?? resolvePath(program.projectRoot, "examples"); - const exampleDir = version ? resolvePath(examplesBaseDir, version) : resolvePath(examplesBaseDir); + const exampleDir = resolveExampleDir(options.examplesDirectory, program.projectRoot, version); if (!(await checkExamplesDirExists(host, exampleDir))) { if (options.examplesDirectory) { diff --git a/packages/typespec-autorest/test/x-ms-examples.test.ts b/packages/typespec-autorest/test/x-ms-examples.test.ts index d94209044f..3784c18268 100644 --- a/packages/typespec-autorest/test/x-ms-examples.test.ts +++ b/packages/typespec-autorest/test/x-ms-examples.test.ts @@ -267,16 +267,17 @@ describe("explicit example", () => { }); it("references source example with relative path in versioned spec and does not copy", async () => { + const emitterOutputDir = resolveVirtualPath("./tsp-output"); + // Examples are placed under the versioned output folder tester.fs.add( - "./examples/v1/getPet.json", - JSON.stringify({ operationId: "Pets_get", title: "Get a pet" }), + resolveVirtualPath("./tsp-output/stable/v1/examples/getPet.json"), + JSON.stringify({ operationId: "Pets_get", title: "Get a pet v1" }), ); tester.fs.add( - "./examples/v2/getPet.json", - JSON.stringify({ operationId: "Pets_get", title: "Get a pet" }), + resolveVirtualPath("./tsp-output/stable/v2/examples/getPet.json"), + JSON.stringify({ operationId: "Pets_get", title: "Get a pet v2" }), ); - const emitterOutputDir = resolveVirtualPath("./tsp-output"); const [{ outputs }, diagnostics] = await tester.compileAndDiagnose( ` @versioned(Versions) @@ -292,6 +293,7 @@ op read(): void; options: { "@azure-tools/typespec-autorest": { "emitter-output-dir": emitterOutputDir, + "examples-dir": `${emitterOutputDir}/{version-status}/{version}/examples`, "skip-example-copying": true, }, }, @@ -303,24 +305,17 @@ op read(): void; const v1Doc = JSON.parse(outputs["stable/v1/openapi.json"]); const v2Doc = JSON.parse(outputs["stable/v2/openapi.json"]); + // $ref should be relative to the swagger file: examples/getPet.json deepStrictEqual(v1Doc.paths["/"]?.get?.["x-ms-examples"], { - "Get a pet": { - $ref: "../../../examples/v1/getPet.json", + "Get a pet v1": { + $ref: "examples/getPet.json", }, }); deepStrictEqual(v2Doc.paths["/"]?.get?.["x-ms-examples"], { - "Get a pet": { - $ref: "../../../examples/v2/getPet.json", + "Get a pet v2": { + $ref: "examples/getPet.json", }, }); - - // Verify examples were NOT copied to the output directory - expect( - tester.fs.fs.has(resolveVirtualPath("./tsp-output/stable/v1/examples/getPet.json")), - ).toBe(false); - expect( - tester.fs.fs.has(resolveVirtualPath("./tsp-output/stable/v2/examples/getPet.json")), - ).toBe(false); }); }); }); diff --git a/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx b/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx index 025fef9d6a..4434ea2186 100644 --- a/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx +++ b/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx @@ -13,29 +13,32 @@ Do not use `@extension("x-ms-examples", "")`. ## Recommended configuration -We recommend enabling the `skip-example-copying` option in your `tspconfig.yaml`. This prevents example files from being copied into the output directory and instead generates `x-ms-examples` `$ref` values that point directly to the source example files using relative paths. +We recommend using `skip-example-copying` together with an `examples-dir` that places examples under the versioned output folder. This keeps example files co-located with the generated swagger and avoids copying files during compilation. ```yaml options: "@azure-tools/typespec-autorest": + emitter-output-dir: "{project-root}" + examples-dir: "{emitter-output-dir}/{version-status}/{version}/examples" skip-example-copying: true ``` -With this setting, example files remain in the source `examples` directory and are referenced in the generated OpenAPI spec via relative paths from the output file location. +With this configuration, example files live alongside the generated swagger at `{version-status}/{version}/examples/` and are referenced via relative paths (e.g., `examples/getPet.json`). ## Example structure -Examples below assume `examples-dir` is `{project-root}/examples` (the default). - -- Single version structure +- Single version structure (versioned API with examples co-located in output) {/* prettier-ignore */} - main.tsp - tspconfig.yaml -- examples/ - - example1.json - - example2.json +- stable/ + - 2021-01-01/ + - openapi.json + - examples/ + - example1.json + - example2.json - Multi version structure @@ -44,13 +47,17 @@ Examples below assume `examples-dir` is `{project-root}/examples` (the default). - main.tsp - tspconfig.yaml -- examples/ +- stable/ - 2021-01-01/ - - example1.json - - example2.json + - openapi.json + - examples/ + - example1.json + - example2.json - 2021-01-02/ - - example1.json - - example2.json + - openapi.json + - examples/ + - example1.json + - example2.json ## Generate the examples @@ -69,5 +76,5 @@ Note, latest OAV tool should automatically generate the following. However, if y - include `operationId`. This is used to match with declared operations in TypeSpec and correctly output in swagger. :::note -When using `skip-example-copying: true`, the generated examples from `oav` will be in the output directory next to `openapi.json`. You should move them to the source `examples` directory in your project root so they are referenced correctly on subsequent compilations. +When using `skip-example-copying: true` with `examples-dir` set to the versioned output folder, `oav generate-examples` will generate examples in the correct location alongside the swagger output. No additional file moves are needed. ::: From ff2350d947dfda75d0af0862642bcac8f2ad8190 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 01:51:14 +0000 Subject: [PATCH 09/12] test: add interpolated examples-dir versioned tests and sub-directory tests Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/21993efe-44da-4d7b-a3b7-cda318fd09b1 Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- .../test/x-ms-examples.test.ts | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/packages/typespec-autorest/test/x-ms-examples.test.ts b/packages/typespec-autorest/test/x-ms-examples.test.ts index 3784c18268..812ae94e15 100644 --- a/packages/typespec-autorest/test/x-ms-examples.test.ts +++ b/packages/typespec-autorest/test/x-ms-examples.test.ts @@ -317,5 +317,136 @@ op read(): void; }, }); }); + + it("picks up version-specific examples with interpolated examples-dir for multiple versions", async () => { + const emitterOutputDir = resolveVirtualPath("./tsp-output"); + // v1 has getPet and listPets; v2 has getPet and deletePet + tester.fs.add( + resolveVirtualPath("./tsp-output/stable/v1/examples/getPet.json"), + JSON.stringify({ operationId: "Pets_get", title: "Get a pet" }), + ); + tester.fs.add( + resolveVirtualPath("./tsp-output/stable/v1/examples/listPets.json"), + JSON.stringify({ operationId: "Pets_list", title: "List pets" }), + ); + tester.fs.add( + resolveVirtualPath("./tsp-output/stable/v2/examples/getPet.json"), + JSON.stringify({ operationId: "Pets_get", title: "Get a pet v2" }), + ); + tester.fs.add( + resolveVirtualPath("./tsp-output/stable/v2/examples/deletePet.json"), + JSON.stringify({ operationId: "Pets_delete", title: "Delete a pet" }), + ); + + const [{ outputs }, diagnostics] = await tester.compileAndDiagnose( + ` +@versioned(Versions) +@service(#{title: "Pet Service"}) +namespace PetService; +enum Versions {v1, v2} + +@route("/pets") +@get +@operationId("Pets_get") +op read(): void; + +@route("/pets") +@post +@operationId("Pets_list") +op list(): void; + +@route("/pets") +@delete +@operationId("Pets_delete") +@added(Versions.v2) +op remove(): void; +`, + { + compilerOptions: { + options: { + "@azure-tools/typespec-autorest": { + "emitter-output-dir": emitterOutputDir, + "examples-dir": `${emitterOutputDir}/{version-status}/{version}/examples`, + "skip-example-copying": true, + }, + }, + }, + }, + ); + expectDiagnosticEmpty(ignoreDiagnostics(diagnostics, ["@typespec/http/no-service-found"])); + + const v1Doc = JSON.parse(outputs["stable/v1/openapi.json"]); + const v2Doc = JSON.parse(outputs["stable/v2/openapi.json"]); + + // v1 should have getPet and listPets examples with correct relative paths + deepStrictEqual(v1Doc.paths["/pets"]?.get?.["x-ms-examples"], { + "Get a pet": { $ref: "examples/getPet.json" }, + }); + deepStrictEqual(v1Doc.paths["/pets"]?.post?.["x-ms-examples"], { + "List pets": { $ref: "examples/listPets.json" }, + }); + + // v2 should have getPet and deletePet examples + deepStrictEqual(v2Doc.paths["/pets"]?.get?.["x-ms-examples"], { + "Get a pet v2": { $ref: "examples/getPet.json" }, + }); + deepStrictEqual(v2Doc.paths["/pets"]?.delete?.["x-ms-examples"], { + "Delete a pet": { $ref: "examples/deletePet.json" }, + }); + }); + + it("picks up examples in sub-directories of the versioned example directory", async () => { + const emitterOutputDir = resolveVirtualPath("./tsp-output"); + // Examples in sub-directories under the versioned examples folder + tester.fs.add( + resolveVirtualPath("./tsp-output/stable/v1/examples/pets/getPet.json"), + JSON.stringify({ operationId: "Pets_get", title: "Get a pet" }), + ); + tester.fs.add( + resolveVirtualPath("./tsp-output/stable/v1/examples/pets/listPets.json"), + JSON.stringify({ operationId: "Pets_list", title: "List all pets" }), + ); + + const [{ outputs }, diagnostics] = await tester.compileAndDiagnose( + ` +@versioned(Versions) +@service(#{title: "Pet Service"}) +namespace PetService; +enum Versions {v1} + +@route("/pets") +@get +@operationId("Pets_get") +op read(): void; + +@route("/pets") +@post +@operationId("Pets_list") +op list(): void; +`, + { + compilerOptions: { + options: { + "@azure-tools/typespec-autorest": { + "emitter-output-dir": emitterOutputDir, + "examples-dir": `${emitterOutputDir}/{version-status}/{version}/examples`, + "skip-example-copying": true, + }, + }, + }, + }, + ); + expectDiagnosticEmpty(ignoreDiagnostics(diagnostics, ["@typespec/http/no-service-found"])); + + const v1Doc = JSON.parse(outputs["stable/v1/openapi.json"]); + + // Sub-directory examples should be picked up with correct relative paths + deepStrictEqual(v1Doc.paths["/pets"]?.get?.["x-ms-examples"], { + "Get a pet": { $ref: "examples/pets/getPet.json" }, + }); + deepStrictEqual(v1Doc.paths["/pets"]?.post?.["x-ms-examples"], { + "List all pets": { $ref: "examples/pets/listPets.json" }, + }); + }); }); }); From e706716098d9f11f6c0c97d0812fb80ba1b45013 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 02:55:51 +0000 Subject: [PATCH 10/12] test: enhance sub-directory examples test with multiple resource sub-dirs and no-copy assertions Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- .../test/x-ms-examples.test.ts | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/typespec-autorest/test/x-ms-examples.test.ts b/packages/typespec-autorest/test/x-ms-examples.test.ts index 812ae94e15..751a1176e5 100644 --- a/packages/typespec-autorest/test/x-ms-examples.test.ts +++ b/packages/typespec-autorest/test/x-ms-examples.test.ts @@ -397,15 +397,24 @@ op remove(): void; it("picks up examples in sub-directories of the versioned example directory", async () => { const emitterOutputDir = resolveVirtualPath("./tsp-output"); - // Examples in sub-directories under the versioned examples folder + // Examples in multiple sub-directories under the versioned examples folder, + // simulating the real-world pattern: stable/2020-01-01/examples/FooResources/*.json tester.fs.add( - resolveVirtualPath("./tsp-output/stable/v1/examples/pets/getPet.json"), + resolveVirtualPath("./tsp-output/stable/v1/examples/PetResources/getPet.json"), JSON.stringify({ operationId: "Pets_get", title: "Get a pet" }), ); tester.fs.add( - resolveVirtualPath("./tsp-output/stable/v1/examples/pets/listPets.json"), + resolveVirtualPath("./tsp-output/stable/v1/examples/PetResources/listPets.json"), JSON.stringify({ operationId: "Pets_list", title: "List all pets" }), ); + tester.fs.add( + resolveVirtualPath("./tsp-output/stable/v1/examples/StoreResources/getStore.json"), + JSON.stringify({ operationId: "Stores_get", title: "Get a store" }), + ); + tester.fs.add( + resolveVirtualPath("./tsp-output/stable/v1/examples/StoreResources/deleteStore.json"), + JSON.stringify({ operationId: "Stores_delete", title: "Delete a store" }), + ); const [{ outputs }, diagnostics] = await tester.compileAndDiagnose( ` @@ -423,6 +432,16 @@ op read(): void; @post @operationId("Pets_list") op list(): void; + +@route("/stores") +@get +@operationId("Stores_get") +op getStore(): void; + +@route("/stores") +@delete +@operationId("Stores_delete") +op deleteStore(): void; `, { compilerOptions: { @@ -440,13 +459,27 @@ op list(): void; const v1Doc = JSON.parse(outputs["stable/v1/openapi.json"]); - // Sub-directory examples should be picked up with correct relative paths + // Sub-directory examples should be picked up with correct relative paths preserving sub-dir deepStrictEqual(v1Doc.paths["/pets"]?.get?.["x-ms-examples"], { - "Get a pet": { $ref: "examples/pets/getPet.json" }, + "Get a pet": { $ref: "examples/PetResources/getPet.json" }, }); deepStrictEqual(v1Doc.paths["/pets"]?.post?.["x-ms-examples"], { - "List all pets": { $ref: "examples/pets/listPets.json" }, + "List all pets": { $ref: "examples/PetResources/listPets.json" }, + }); + deepStrictEqual(v1Doc.paths["/stores"]?.get?.["x-ms-examples"], { + "Get a store": { $ref: "examples/StoreResources/getStore.json" }, }); + deepStrictEqual(v1Doc.paths["/stores"]?.delete?.["x-ms-examples"], { + "Delete a store": { $ref: "examples/StoreResources/deleteStore.json" }, + }); + + // Verify no files were copied to a separate examples output + expect( + tester.fs.fs.has(resolveVirtualPath("./tsp-output/examples/PetResources/getPet.json")), + ).toBe(false); + expect( + tester.fs.fs.has(resolveVirtualPath("./tsp-output/examples/StoreResources/getStore.json")), + ).toBe(false); }); }); }); From b16c2311f4ac1aeb1b88c534bff7f61946df1eed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 May 2026 22:31:31 +0000 Subject: [PATCH 11/12] revert: remove unnecessary package.json version pinning in standalone snapshots Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- .../azure-arm_stand_alone/package.json | 20 +++++++++---------- .../azure-core_stand_alone/package.json | 20 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/eng/feeds/__snapshots__/azure-arm_stand_alone/package.json b/eng/feeds/__snapshots__/azure-arm_stand_alone/package.json index f9198f892a..8e1ec45eae 100644 --- a/eng/feeds/__snapshots__/azure-arm_stand_alone/package.json +++ b/eng/feeds/__snapshots__/azure-arm_stand_alone/package.json @@ -4,15 +4,15 @@ "type": "module", "private": true, "dependencies": { - "@typespec/compiler": "^1.11.0", - "@azure-tools/typespec-autorest": "^0.67.0", - "@azure-tools/typespec-azure-core": "^0.67.1", - "@azure-tools/typespec-azure-resource-manager": "^0.67.1", - "@azure-tools/typespec-providerhub": "^0.50.0", - "@azure-tools/typespec-azure-rulesets": "^0.67.0", - "@typespec/http": "^1.11.0", - "@typespec/openapi": "^1.11.0", - "@typespec/rest": "^0.81.0", - "@typespec/versioning": "^0.81.0" + "@typespec/compiler": "latest", + "@azure-tools/typespec-autorest": "latest", + "@azure-tools/typespec-azure-core": "latest", + "@azure-tools/typespec-azure-resource-manager": "latest", + "@azure-tools/typespec-providerhub": "latest", + "@azure-tools/typespec-azure-rulesets": "latest", + "@typespec/http": "latest", + "@typespec/openapi": "latest", + "@typespec/rest": "latest", + "@typespec/versioning": "latest" } } \ No newline at end of file diff --git a/eng/feeds/__snapshots__/azure-core_stand_alone/package.json b/eng/feeds/__snapshots__/azure-core_stand_alone/package.json index d54b5c0ecb..75914e24ea 100644 --- a/eng/feeds/__snapshots__/azure-core_stand_alone/package.json +++ b/eng/feeds/__snapshots__/azure-core_stand_alone/package.json @@ -4,15 +4,15 @@ "type": "module", "private": true, "dependencies": { - "@typespec/compiler": "^1.11.0", - "@azure-tools/typespec-autorest": "^0.67.0", - "@azure-tools/typespec-azure-core": "^0.67.1", - "@azure-tools/typespec-azure-resource-manager": "^0.67.1", - "@azure-tools/typespec-client-generator-core": "^0.67.4", - "@azure-tools/typespec-azure-rulesets": "^0.67.0", - "@typespec/http": "^1.11.0", - "@typespec/openapi": "^1.11.0", - "@typespec/rest": "^0.81.0", - "@typespec/versioning": "^0.81.0" + "@typespec/compiler": "latest", + "@azure-tools/typespec-autorest": "latest", + "@azure-tools/typespec-azure-core": "latest", + "@azure-tools/typespec-azure-resource-manager": "latest", + "@azure-tools/typespec-client-generator-core": "latest", + "@azure-tools/typespec-azure-rulesets": "latest", + "@typespec/http": "latest", + "@typespec/openapi": "latest", + "@typespec/rest": "latest", + "@typespec/versioning": "latest" } } \ No newline at end of file From 8c841640b7941b95da4703d4dd45daf4b930c13e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 May 2026 01:43:02 +0000 Subject: [PATCH 12/12] docs: apply suggested x-ms-examples doc changes (skip-example-copying intro + output-file setting) Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- .../content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx b/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx index 4434ea2186..f34e979ad7 100644 --- a/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx +++ b/website/src/content/docs/docs/migrate-swagger/faq/x-ms-examples.mdx @@ -5,7 +5,7 @@ title: x-ms-examples example files import { FileTree } from "@astrojs/starlight/components"; The `x-ms-examples` is automatically populated in the generated OpenAPI 2.0 when using the `typespec-autorest` emitter. -The examples must be placed in the `examples-dir` directory (default `{project-root}/examples`) and have the `operationId` property. +If you use the `skip-example-copying` in typespec-autorest configuration as suggested below, examples will automatically be picked up from the examples directory adjacent to the spec. If you do not use this option, the examples must be placed in the `examples-dir` directory (default `{project-root}/examples`). Examples must all have the `operationId` property, allowing each example to be matched with the appropriate operation. :::caution Do not use `@extension("x-ms-examples", "")`. @@ -19,6 +19,7 @@ We recommend using `skip-example-copying` together with an `examples-dir` that p options: "@azure-tools/typespec-autorest": emitter-output-dir: "{project-root}" + output-file: "{emitter-output-dir}/{version-status}/{version}/openapi.json" examples-dir: "{emitter-output-dir}/{version-status}/{version}/examples" skip-example-copying: true ```