Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0ce39ea
feat(typespec-metadata): add apiVersion field using TCGC resolution
Jul 27, 2026
b9bf362
chore: update pnpm-lock.yaml for new TCGC peer dependency
Jul 27, 2026
e8302ac
test(typespec-metadata): add e2e tests for apiVersion resolution
Jul 27, 2026
14d38da
test(typespec-metadata): rewrite e2e tests to verify emitted metadata…
Jul 27, 2026
427b365
chore: add changeset for apiVersion feature
Jul 27, 2026
4aa538a
refactor: remove redundant apiVersion unit tests from collector.test.ts
Jul 27, 2026
3a5a9e0
style: apply prettier formatting
Jul 27, 2026
2a8df3d
fix: revert pnpm-lock.yaml to upstream/main
Jul 27, 2026
0e879fb
chore: revert manual lockfile edit (will regenerate in cloud)
Jul 27, 2026
75d83ab
chore: regenerate pnpm-lock.yaml for new typespec-metadata deps
Jul 27, 2026
897c048
refactor: restructure test tester into ApiTester/MetadataTester/Emitt…
Jul 27, 2026
e6112f4
refactor: remove api-version emitter option, use additionalProperties…
Jul 27, 2026
034d522
fix: check sdkContext.apiVersion for 'all' before resolving from meta…
Jul 27, 2026
a8865ba
revert: restore additionalProperties: false in metadata options schema
Jul 27, 2026
1ed30f1
test: set api-version 'all' on typespec-python instead of typespec-me…
Jul 27, 2026
ef4b4bc
feat(tcgc): add isPreview to SdkPackageType metadata
Jul 27, 2026
75b4fc4
Revert "feat(tcgc): add isPreview to SdkPackageType metadata"
Jul 27, 2026
f82a9fb
Merge remote-tracking branch 'upstream/main' into iscai-msft-stunning…
Jul 27, 2026
dadf0f5
feat(typespec-metadata): add sdkType field (preview/stable) to metadata
Jul 27, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-metadata"
---

Add `apiVersion` field to `LanguagePackageMetadata` resolved via TCGC's `createSdkContext`. Values: `"all"` when configured for all versions, `"multiple-versions"` for multi-service configs, the actual resolved version string, or `undefined` when unavailable. Also adds `api-version` as a passthrough emitter option.
5 changes: 5 additions & 0 deletions packages/typespec-metadata/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,16 @@
"yaml": "catalog:"
},
"peerDependencies": {
"@azure-tools/typespec-client-generator-core": "workspace:^",
"@typespec/compiler": "workspace:^"
},
"devDependencies": {
"@azure-tools/typespec-client-generator-core": "workspace:^",
"@types/node": "catalog:",
"@typespec/compiler": "workspace:^",
"@typespec/http": "workspace:^",
"@typespec/rest": "workspace:^",
"@typespec/versioning": "workspace:^",
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:",
"rimraf": "catalog:",
Expand Down
7 changes: 6 additions & 1 deletion packages/typespec-metadata/src/collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ export interface LanguageCollectionResult {
export async function collectLanguagePackages(
program: Program,
baseOutputDir: string,
resolvedApiVersion?: string,
): Promise<LanguageCollectionResult> {
const optionMap = program.compilerOptions.options ?? {};
const params = extractParameters(optionMap);
Expand All @@ -336,7 +337,7 @@ export async function collectLanguagePackages(
}

return {
languages: buildLanguageMetadata(optionMap, params, baseOutputDir, defaultServiceDir),
languages: buildLanguageMetadata(optionMap, params, baseOutputDir, defaultServiceDir, resolvedApiVersion),
sourceConfigPath: program.compilerOptions.config,
};
}
Expand Down Expand Up @@ -483,6 +484,7 @@ export function buildLanguageMetadata(
params: Record<string, unknown>,
baseOutputDir: string,
defaultServiceDir?: string,
resolvedApiVersion?: string,
): Record<string, LanguagePackageMetadata[]> {
const languagesDict: Record<string, LanguagePackageMetadata[]> = {};

Expand All @@ -493,6 +495,7 @@ export function buildLanguageMetadata(
params,
baseOutputDir,
defaultServiceDir,
resolvedApiVersion,
);
const language = inferLanguageFromEmitterName(emitterName);
if (!languagesDict[language]) {
Expand All @@ -510,6 +513,7 @@ function createLanguageMetadata(
params: Record<string, unknown>,
baseOutputDir: string,
defaultServiceDir?: string,
resolvedApiVersion?: string,
): LanguagePackageMetadata {
const normalizedOptions = normalizeOptionsObject(emitterOptions);

Expand Down Expand Up @@ -585,6 +589,7 @@ function createLanguageMetadata(
outputDir: relativeOutputDir,
flavor: flavor ? String(flavor) : undefined,
serviceDir,
apiVersion: resolvedApiVersion,
};
}

Expand Down
19 changes: 18 additions & 1 deletion packages/typespec-metadata/src/emitter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { emitFile, getDirectoryPath, resolvePath, type EmitContext } from "@typespec/compiler";
import { createSdkContext } from "@azure-tools/typespec-client-generator-core";
import { stringify as stringifyYaml } from "yaml";
import packageJson from "../package.json" with { type: "json" };
import { buildSpecMetadata, collectLanguagePackages } from "./collector.js";
Expand All @@ -18,7 +19,23 @@ export async function $onEmit(context: EmitContext<MetadataEmitterOptions>): Pro
// Get the common tsp-output directory (parent of this emitter's output dir)
const commonOutputDir = getDirectoryPath(getDirectoryPath(context.emitterOutputDir));

const languageResult = await collectLanguagePackages(context.program, commonOutputDir);
// Resolve API version using TCGC
const sdkContext = await createSdkContext(context as any);
const apiVersionsMap = sdkContext.sdkPackage.metadata.apiVersions;
let resolvedApiVersion: string | undefined;
if (apiVersionsMap && apiVersionsMap.size > 0) {
if (apiVersionsMap.size > 1) {
resolvedApiVersion = "multiple-versions";
} else {
resolvedApiVersion = [...apiVersionsMap.values()][0];
}
}

const languageResult = await collectLanguagePackages(
context.program,
commonOutputDir,
resolvedApiVersion,
);

const snapshot: MetadataSnapshot = {
emitterVersion: SNAPSHOT_VERSION,
Expand Down
8 changes: 8 additions & 0 deletions packages/typespec-metadata/src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export interface LanguagePackageMetadata {
flavor?: string;
/** Service directory path for this language emitter. */
serviceDir?: string;
/**
* Resolved API version for this emitter.
* - `"all"` when configured for all versions
* - `"multiple-versions"` for multi-service configs with more than one API version entry
* - An actual resolved version string (e.g. `"2023-10-01"`) for a single version
* - `undefined` when no API version information is available
*/
apiVersion?: string;

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.

Should this be in language metadata or typespec metadata? Is it possible to have different API version value for each language?

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.

you're right, I guess each language could specify different values in tspconfig.yaml, so it shouldn't be the case but it could be

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.

would you prefer to ignore that possibility and move it to typespec metadata, or keep it where it is in language metadata

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.

Nope. Let's keep it in language metadata. It's easier to parse all get the distinct on the consumer side.

}

export interface MetadataSnapshot {
Expand Down
6 changes: 6 additions & 0 deletions packages/typespec-metadata/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export interface MetadataEmitterOptions {
* Serialization format for the metadata snapshot.
*/
format?: MetadataOutputFormat;
/**
* API version configuration passed through to TCGC for version resolution.
* Can be "all", a specific version string, or a per-service map.
*/
"api-version"?: string;
}

export interface NormalizedMetadataEmitterOptions {
Expand All @@ -30,6 +35,7 @@ export const metadataEmitterOptionsSchema: JSONSchemaType<MetadataEmitterOptions
properties: {
outputFile: { type: "string", nullable: true },
format: { type: "string", enum: ["yaml", "json"], nullable: true },
"api-version": { type: "string", nullable: true },
},
};

Expand Down
163 changes: 163 additions & 0 deletions packages/typespec-metadata/test/api-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { parse as parseYaml } from "yaml";
import { describe, expect, it } from "vitest";
import type { MetadataSnapshot } from "../src/metadata.js";
import { SimpleTester } from "./tester.js";

function emitMetadata(code: string, compilerOptions: Record<string, unknown> = {}) {
return SimpleTester.emit("@azure-tools/typespec-metadata", {}).compileAndDiagnose(code, {
compilerOptions: {
options: {
"@azure-tools/typespec-python": {
"package-name": "azure-test-service",
},
...((compilerOptions.options as Record<string, unknown>) ?? {}),
},
...Object.fromEntries(
Object.entries(compilerOptions).filter(([k]) => k !== "options"),
),
},
});
}

function parseMetadata(outputs: Record<string, string>): MetadataSnapshot {
const content = Object.values(outputs)[0];
return parseYaml(content) as MetadataSnapshot;
}

describe("apiVersion in emitted metadata", () => {
it("single versioned service emits latest version as apiVersion", async () => {
const [{ outputs }] = await emitMetadata(`
@service(#{
title: "Widget Service",
})
@versioned(WidgetService.Versions)
namespace WidgetService;

enum Versions {
v1,
v2,
v3,
}

op test(): void;
`);

const snapshot = parseMetadata(outputs);
const pythonMeta = snapshot.languages["python"];
expect(pythonMeta).toBeDefined();
expect(pythonMeta[0].apiVersion).toBe("v3");
});

it("service with api-version 'all' emits 'all' as apiVersion", async () => {
const [{ outputs }] = await SimpleTester.emit("@azure-tools/typespec-metadata", {}).compileAndDiagnose(
`
@service(#{
title: "Widget Service",
})
@versioned(WidgetService.Versions)
namespace WidgetService;

enum Versions {
v1,
v2,
v3,
}

op test(): void;
`,
{
compilerOptions: {
options: {
"@azure-tools/typespec-metadata": { "api-version": "all" },
"@azure-tools/typespec-python": {
"package-name": "azure-test-service",
},
},
},
},
);

const snapshot = parseMetadata(outputs);
expect(snapshot.languages["python"][0].apiVersion).toBe("all");
});

it("service without versioning emits undefined apiVersion", async () => {
const [{ outputs }] = await emitMetadata(`
@service(#{
title: "Widget Service",
})
namespace WidgetService;

op test(): void;
`);

const snapshot = parseMetadata(outputs);
expect(snapshot.languages["python"][0].apiVersion).toBeUndefined();
});

it("multiple services emit 'multiple-versions' as apiVersion", async () => {
const [{ outputs }] = await emitMetadata(`
@service
@versioned(VersionsA)
namespace ServiceA {
enum VersionsA {
av1,
av2,
}
interface AI {
@route("/aTest")
aTest(@query("api-version") apiVersion: VersionsA): void;
}
}
@service
@versioned(VersionsB)
namespace ServiceB {
enum VersionsB {
bv1,
bv2,
}
interface BI {
@route("/bTest")
bTest(@query("api-version") apiVersion: VersionsB): void;
}
}
`);

const snapshot = parseMetadata(outputs);
const pythonMeta = snapshot.languages["python"];
expect(pythonMeta[0].apiVersion).toBe("multiple-versions");
});

it("apiVersion is applied to all language emitters", async () => {
const [{ outputs }] = await emitMetadata(
`
@service(#{
title: "Widget Service",
})
@versioned(WidgetService.Versions)
namespace WidgetService;

enum Versions {
v1,
v2,
}

op test(): void;
`,
{
options: {
"@azure-tools/typespec-python": {
"package-name": "azure-test-service",
},
"@azure-tools/typespec-java": {
"package-name": "com.azure:azure-test-service",
},
},
},
);

const snapshot = parseMetadata(outputs);
expect(snapshot.languages["python"][0].apiVersion).toBe("v2");
expect(snapshot.languages["java"][0].apiVersion).toBe("v2");
});
});
19 changes: 19 additions & 0 deletions packages/typespec-metadata/test/tester.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { resolvePath } from "@typespec/compiler";
import { createTester } from "@typespec/compiler/testing";

export const MetadataTester = createTester(resolvePath(import.meta.dirname, ".."), {
Comment thread
iscai-msft marked this conversation as resolved.
Outdated
libraries: [
"@typespec/http",
"@typespec/rest",
"@typespec/versioning",
"@azure-tools/typespec-client-generator-core",
"@azure-tools/typespec-metadata",
],
});

export const SimpleTester = MetadataTester.import(
"@typespec/http",
"@typespec/rest",
"@typespec/versioning",
"@azure-tools/typespec-client-generator-core",
).using("Http", "Rest", "Versioning", "Azure.ClientGenerator.Core");
Loading
Loading