From db375e1e035d3c07d51517a3e7324bbfd96d3803 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Fri, 1 Aug 2025 12:11:53 -0700 Subject: [PATCH 01/12] add decorator and TK --- .../generated-defs/TypeSpec.HttpClient.ts | 38 +++++++++ .../TypeSpec.HttpClient.ts-test.ts | 10 +++ packages/http-client/lib/common.tsp | 5 ++ .../http-client/lib/feature-lifecycle.tsp | 28 +++++++ packages/http-client/lib/main.tsp | 2 + packages/http-client/package.json | 14 +++- packages/http-client/src/decorators/common.ts | 0 .../src/decorators/feature-lifecycle.ts | 53 ++++++++++++ packages/http-client/src/decorators/index.ts | 1 + .../http-client/src/decorators/scope-cache.ts | 80 +++++++++++++++++++ packages/http-client/src/lib.ts | 2 +- packages/http-client/src/tsp-index.ts | 9 +++ .../http-client/src/typekit/kits/client.ts | 14 ++++ .../test/feature-lifecycle.test.ts | 17 ++++ packages/http-client/test/test-host.ts | 9 ++- 15 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 packages/http-client/generated-defs/TypeSpec.HttpClient.ts create mode 100644 packages/http-client/generated-defs/TypeSpec.HttpClient.ts-test.ts create mode 100644 packages/http-client/lib/common.tsp create mode 100644 packages/http-client/lib/feature-lifecycle.tsp create mode 100644 packages/http-client/lib/main.tsp create mode 100644 packages/http-client/src/decorators/common.ts create mode 100644 packages/http-client/src/decorators/feature-lifecycle.ts create mode 100644 packages/http-client/src/decorators/index.ts create mode 100644 packages/http-client/src/decorators/scope-cache.ts create mode 100644 packages/http-client/src/tsp-index.ts create mode 100644 packages/http-client/test/feature-lifecycle.test.ts diff --git a/packages/http-client/generated-defs/TypeSpec.HttpClient.ts b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts new file mode 100644 index 00000000000..7139c40644e --- /dev/null +++ b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts @@ -0,0 +1,38 @@ +import type { + DecoratorContext, + Enum, + EnumMember, + Interface, + Model, + ModelProperty, + Namespace, + Operation, + Scalar, + Union, + UnionVariant, +} from "@typespec/compiler"; + +export interface FeatureLifecycleOptions { + readonly emitterScope?: string; +} + +export type FeatureLifecycleDecorator = ( + context: DecoratorContext, + target: + | Model + | ModelProperty + | Operation + | Interface + | Namespace + | Union + | UnionVariant + | Enum + | EnumMember + | Scalar, + value: EnumMember, + options?: FeatureLifecycleOptions, +) => void; + +export type TypeSpecHttpClientDecorators = { + featureLifecycle: FeatureLifecycleDecorator; +}; diff --git a/packages/http-client/generated-defs/TypeSpec.HttpClient.ts-test.ts b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts-test.ts new file mode 100644 index 00000000000..619fd1f91e9 --- /dev/null +++ b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts-test.ts @@ -0,0 +1,10 @@ +// An error in the imports would mean that the decorator is not exported or +// doesn't have the right name. + +import { $decorators } from "@typespec/http-client"; +import type { TypeSpecHttpClientDecorators } from "./TypeSpec.HttpClient.js"; + +/** + * An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... + */ +const _: TypeSpecHttpClientDecorators = $decorators["TypeSpec.HttpClient"]; diff --git a/packages/http-client/lib/common.tsp b/packages/http-client/lib/common.tsp new file mode 100644 index 00000000000..13721e82bd3 --- /dev/null +++ b/packages/http-client/lib/common.tsp @@ -0,0 +1,5 @@ +namespace TypeSpec.HttpClient; + +model ClientDecoratorOptions { + emitterScope?: string; +} diff --git a/packages/http-client/lib/feature-lifecycle.tsp b/packages/http-client/lib/feature-lifecycle.tsp new file mode 100644 index 00000000000..8005edf681b --- /dev/null +++ b/packages/http-client/lib/feature-lifecycle.tsp @@ -0,0 +1,28 @@ +import "@typespec/compiler"; +import "./common.tsp"; + +namespace TypeSpec.HttpClient; + +enum FeatureLifecycle { + Stable: "stable", +} + +model FeatureLifecycleOptions { + ...ClientDecoratorOptions; +} + +extern dec featureLifecycle( + target: + | Reflection.Model + | Reflection.ModelProperty + | Reflection.Operation + | Reflection.Interface + | Reflection.Namespace + | Reflection.Union + | Reflection.UnionVariant + | Reflection.Enum + | Reflection.EnumMember + | Reflection.Scalar, + value: Reflection.EnumMember, + options?: valueof FeatureLifecycleOptions +); diff --git a/packages/http-client/lib/main.tsp b/packages/http-client/lib/main.tsp new file mode 100644 index 00000000000..88baffe5afd --- /dev/null +++ b/packages/http-client/lib/main.tsp @@ -0,0 +1,2 @@ +import "./feature-lifecycle.tsp"; +import "../dist/src/tsp-index.js"; diff --git a/packages/http-client/package.json b/packages/http-client/package.json index 9e87e0efb50..93763ba4ea9 100644 --- a/packages/http-client/package.json +++ b/packages/http-client/package.json @@ -4,12 +4,16 @@ "type": "module", "main": "dist/src/index.js", "license": "MIT", + "tspMain": "./lib/main.tsp", "exports": { ".": { - "import": "./dist/src/index.js" + "typespec": "./lib/main.tsp", + "types": "./dist/src/index.d.ts", + "default": "./dist/src/index.js" }, "./testing": { - "import": "./dist/src/testing/index.js" + "types": "./dist/src/testing/index.d.ts", + "default": "./dist/src/testing/index.js" }, "./typekit": { "import": "./dist/src/typekit/index.js" @@ -31,17 +35,21 @@ "@alloy-js/rollup-plugin": "^0.1.0", "@alloy-js/typescript": "^0.19.0", "@types/node": "~24.1.0", + "@typespec/tspd": "workspace:^", "@typespec/compiler": "workspace:^", "@typespec/emitter-framework": "workspace:^", "@typespec/http": "workspace:^", + "@typespec/library-linter": "workspace:^", "eslint": "^9.23.0", "prettier": "~3.6.2", "typescript": "~5.8.2", "vitest": "^3.1.2" }, "scripts": { - "build": "alloy build", + "build": "npm run gen-extern-signature && alloy build && npm run lint-typespec-library", + "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", "clean": "rimraf ./dist", + "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "watch": "alloy build --watch", "test": "vitest run", "test:ui": "vitest --ui", diff --git a/packages/http-client/src/decorators/common.ts b/packages/http-client/src/decorators/common.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/http-client/src/decorators/feature-lifecycle.ts b/packages/http-client/src/decorators/feature-lifecycle.ts new file mode 100644 index 00000000000..f96eafc79d6 --- /dev/null +++ b/packages/http-client/src/decorators/feature-lifecycle.ts @@ -0,0 +1,53 @@ +import { Program, Type } from "@typespec/compiler"; +import { FeatureLifecycleDecorator } from "../../generated-defs/TypeSpec.HttpClient.js"; +import { createStateSymbol } from "../lib.js"; +import { parseScopeFilter, useClientStateMap } from "./scope-cache.js"; + +const featureLifecycleStateSymbol = createStateSymbol("featureLifecycleState"); + +const [getFeatureLifecycleState, setFeatureLifecycleState] = useClientStateMap( + featureLifecycleStateSymbol, +); + +export const $featureLifecycle: FeatureLifecycleDecorator = (context, target, value, options) => { + const scopeFilter = parseScopeFilter(options?.emitterScope); + + setFeatureLifecycleState(context.program, target, { + emitterFilter: scopeFilter, + value: (value.value && String(value.value)) || value.name, + }); +}; + +export interface GetFeatureLifecycleOptions { + emitterName?: string; +} +export function getClientFeatureLifecycle( + program: Program, + target: Type, + options: GetFeatureLifecycleOptions = {}, +): string | undefined { + const lifecycle = getFeatureLifecycleState(program, target); + + if (!lifecycle) { + return undefined; + } + + const emitterScope = options.emitterName; + const lifecycleValue = lifecycle.value; + + if (!emitterScope) { + return lifecycle?.emitterFilter.isUnscoped ? lifecycleValue : undefined; + } + + if (lifecycle.emitterFilter.includedEmitters.length) { + return lifecycle.emitterFilter.includedEmitters.includes(emitterScope) + ? lifecycleValue + : undefined; + } + + if (lifecycle.emitterFilter.excludedEmitters.length) { + return lifecycle.emitterFilter.excludedEmitters.includes(emitterScope) + ? undefined + : lifecycleValue; + } +} diff --git a/packages/http-client/src/decorators/index.ts b/packages/http-client/src/decorators/index.ts new file mode 100644 index 00000000000..68ce9337d3e --- /dev/null +++ b/packages/http-client/src/decorators/index.ts @@ -0,0 +1 @@ +export * from "./feature-lifecycle.js"; diff --git a/packages/http-client/src/decorators/scope-cache.ts b/packages/http-client/src/decorators/scope-cache.ts new file mode 100644 index 00000000000..5c25a736e89 --- /dev/null +++ b/packages/http-client/src/decorators/scope-cache.ts @@ -0,0 +1,80 @@ +import { Program } from "@typespec/compiler"; + +export interface EmitterFilter { + includedEmitters: string[]; + excludedEmitters: string[]; + isUnscoped: boolean; +} + +export interface ScopedValue { + emitterFilter: EmitterFilter; + value: T; +} + +const clientStateMap = new WeakMap>>>(); + +export function useClientStateMap( + symbol: symbol, +): [ + get: (program: Program, key: K) => ScopedValue | undefined, + set: (program: Program, key: K, value: ScopedValue) => Map>, + entries: (program: Program) => IterableIterator<[K, ScopedValue]>, +] { + type InnerMap = Map>; + // Ensure we have a map for this program & symbol + function getInnerMap(program: Program): InnerMap { + let stateMap = clientStateMap.get(program); + if (!stateMap) { + stateMap = new Map(); + clientStateMap.set(program, stateMap); + } + + if (!stateMap.has(symbol)) { + stateMap.set(symbol, new Map>()); + } + return stateMap.get(symbol)!; + } + + const get = (program: Program, key: K): ScopedValue | undefined => { + return getInnerMap(program).get(key); + }; + + const set = (program: Program, key: K, value: ScopedValue): Map> => { + return getInnerMap(program).set(key, value); + }; + + const entries = (program: Program): IterableIterator<[K, ScopedValue]> => { + return getInnerMap(program).entries(); + }; + + return [get, set, entries]; +} + +export function parseScopeFilter(string: string | undefined): EmitterFilter { + if (!string) { + return { + excludedEmitters: [], + includedEmitters: [], + isUnscoped: true, + }; + } + + const parts = string.split(","); + const includedEmitters: string[] = []; + const excludedEmitters: string[] = []; + + for (const part of parts) { + const trimmed = part.trim(); + if (trimmed.startsWith("!")) { + excludedEmitters.push(trimmed.substring(1)); + } else { + includedEmitters.push(trimmed); + } + } + + return { + excludedEmitters, + includedEmitters, + isUnscoped: false, + }; +} diff --git a/packages/http-client/src/lib.ts b/packages/http-client/src/lib.ts index f683687f0df..797b2f56e7e 100644 --- a/packages/http-client/src/lib.ts +++ b/packages/http-client/src/lib.ts @@ -39,4 +39,4 @@ export const $lib = createTypeSpecLibrary({ }, }); -export const { reportDiagnostic, createDiagnostic, stateKeys: StateKeys } = $lib; +export const { reportDiagnostic, createDiagnostic, stateKeys: StateKeys, createStateSymbol } = $lib; diff --git a/packages/http-client/src/tsp-index.ts b/packages/http-client/src/tsp-index.ts new file mode 100644 index 00000000000..bc46f5954b6 --- /dev/null +++ b/packages/http-client/src/tsp-index.ts @@ -0,0 +1,9 @@ +import { TypeSpecHttpClientDecorators } from "../generated-defs/TypeSpec.HttpClient.js"; + +import { $featureLifecycle } from "./decorators/index.js"; + +export const $decorators = { + "TypeSpec.HttpClient": { + featureLifecycle: $featureLifecycle, + } as TypeSpecHttpClientDecorators, +}; diff --git a/packages/http-client/src/typekit/kits/client.ts b/packages/http-client/src/typekit/kits/client.ts index 8d2842a27e7..8d1c8399f80 100644 --- a/packages/http-client/src/typekit/kits/client.ts +++ b/packages/http-client/src/typekit/kits/client.ts @@ -6,6 +6,7 @@ import { Namespace, NoTarget, Operation, + Type, } from "@typespec/compiler"; import { defineKit } from "@typespec/compiler/typekit"; import { @@ -17,6 +18,10 @@ import { resolveAuthentication, } from "@typespec/http"; import "@typespec/http/experimental/typekit"; +import { + getClientFeatureLifecycle, + GetFeatureLifecycleOptions, +} from "../../decorators/feature-lifecycle.js"; import { InternalClient } from "../../interfaces.js"; import { reportDiagnostic } from "../../lib.js"; import { createBaseConstructor, getConstructors } from "../../utils/client-helpers.js"; @@ -24,6 +29,12 @@ import { getStringValue } from "../../utils/helpers.js"; import { NameKit } from "./utils.js"; interface ClientKit extends NameKit { + /** + * Get the feature lifecycle value for a given type + * @param type The type to get the feature lifecycle for + * @param options The options to use when getting the feature lifecycle + */ + getFeatureLifecycle(type: Type, options?: GetFeatureLifecycleOptions): string | undefined; /** * Get the parent of a client * @param type The client to get the parent of @@ -98,6 +109,9 @@ export const clientOperationCache = new Map(); defineKit({ client: { + getFeatureLifecycle(type, options) { + return getClientFeatureLifecycle(this.program, type, options); + }, getParent(client) { const type = client.kind === "Client" ? client.type : client; if (type.namespace && type.namespace !== this.program.getGlobalNamespaceType()) { diff --git a/packages/http-client/test/feature-lifecycle.test.ts b/packages/http-client/test/feature-lifecycle.test.ts new file mode 100644 index 00000000000..ecfb9d0cc21 --- /dev/null +++ b/packages/http-client/test/feature-lifecycle.test.ts @@ -0,0 +1,17 @@ +import { t } from "@typespec/compiler/testing"; +import { expect, it } from "vitest"; +import { Tester } from "./test-host.js"; + +it("should get the feature lifecycle for a model property", async () => { + const { betaProp } = await Tester.compile(t.code` + namespace Test; + + model MyModel { + id: string; + @featureLifecycle(FeatureLifecycle.Experimental) + ${t.modelProperty("betaProp")}: string; + } + `); + + expect(betaProp.kind).toBe("modelProperty"); +}); diff --git a/packages/http-client/test/test-host.ts b/packages/http-client/test/test-host.ts index 34916cb1d1b..101b2a6541e 100644 --- a/packages/http-client/test/test-host.ts +++ b/packages/http-client/test/test-host.ts @@ -1,6 +1,13 @@ -import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; +import { resolvePath } from "@typespec/compiler"; +import { createTestHost, createTestWrapper, createTester } from "@typespec/compiler/testing"; import { HttpTestLibrary } from "@typespec/http/testing"; +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/http", "@typespec/http-client"], +}) + .importLibraries() + .using("HttpClient"); + export async function createTypespecHttpClientLibraryTestHost() { return createTestHost({ libraries: [HttpTestLibrary], From a5c68f1cc0d95768e41c2ef21a5f69a0f314af36 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Fri, 1 Aug 2025 14:07:11 -0700 Subject: [PATCH 02/12] Introduce featureLifecycle decorator and tests --- .../{feature-lifecycle.tsp => decorators.tsp} | 4 +- packages/http-client/lib/main.tsp | 4 +- .../src/decorators/feature-lifecycle.ts | 10 +- .../http-client/src/decorators/scope-cache.ts | 70 +++++----- packages/http-client/src/index.ts | 3 + packages/http-client/src/testing/index.ts | 14 +- packages/http-client/src/tsp-index.ts | 2 +- .../src/types/typespec-augmentations.d.ts | 2 +- .../test/feature-lifecycle.test.ts | 124 +++++++++++++++++- packages/http-client/test/test-host.ts | 7 +- 10 files changed, 188 insertions(+), 52 deletions(-) rename packages/http-client/lib/{feature-lifecycle.tsp => decorators.tsp} (91%) diff --git a/packages/http-client/lib/feature-lifecycle.tsp b/packages/http-client/lib/decorators.tsp similarity index 91% rename from packages/http-client/lib/feature-lifecycle.tsp rename to packages/http-client/lib/decorators.tsp index 8005edf681b..ecae1f37466 100644 --- a/packages/http-client/lib/feature-lifecycle.tsp +++ b/packages/http-client/lib/decorators.tsp @@ -1,10 +1,10 @@ -import "@typespec/compiler"; import "./common.tsp"; +import "../dist/src/tsp-index.js"; namespace TypeSpec.HttpClient; enum FeatureLifecycle { - Stable: "stable", + Experimental, } model FeatureLifecycleOptions { diff --git a/packages/http-client/lib/main.tsp b/packages/http-client/lib/main.tsp index 88baffe5afd..b13f25fa234 100644 --- a/packages/http-client/lib/main.tsp +++ b/packages/http-client/lib/main.tsp @@ -1,2 +1,4 @@ -import "./feature-lifecycle.tsp"; +import "./decorators.tsp"; import "../dist/src/tsp-index.js"; + +namespace TypeSpec.HttpClient; diff --git a/packages/http-client/src/decorators/feature-lifecycle.ts b/packages/http-client/src/decorators/feature-lifecycle.ts index f96eafc79d6..e78d7aa97c1 100644 --- a/packages/http-client/src/decorators/feature-lifecycle.ts +++ b/packages/http-client/src/decorators/feature-lifecycle.ts @@ -1,17 +1,17 @@ import { Program, Type } from "@typespec/compiler"; +import { useStateMap } from "@typespec/compiler/utils"; import { FeatureLifecycleDecorator } from "../../generated-defs/TypeSpec.HttpClient.js"; import { createStateSymbol } from "../lib.js"; -import { parseScopeFilter, useClientStateMap } from "./scope-cache.js"; +import { parseScopeFilter, ScopedValue } from "./scope-cache.js"; const featureLifecycleStateSymbol = createStateSymbol("featureLifecycleState"); -const [getFeatureLifecycleState, setFeatureLifecycleState] = useClientStateMap( +const [getFeatureLifecycleState, setFeatureLifecycleState] = useStateMap>( featureLifecycleStateSymbol, ); export const $featureLifecycle: FeatureLifecycleDecorator = (context, target, value, options) => { const scopeFilter = parseScopeFilter(options?.emitterScope); - setFeatureLifecycleState(context.program, target, { emitterFilter: scopeFilter, value: (value.value && String(value.value)) || value.name, @@ -35,6 +35,10 @@ export function getClientFeatureLifecycle( const emitterScope = options.emitterName; const lifecycleValue = lifecycle.value; + if (lifecycle.emitterFilter.isUnscoped) { + return lifecycle.value; + } + if (!emitterScope) { return lifecycle?.emitterFilter.isUnscoped ? lifecycleValue : undefined; } diff --git a/packages/http-client/src/decorators/scope-cache.ts b/packages/http-client/src/decorators/scope-cache.ts index 5c25a736e89..14fd447acb3 100644 --- a/packages/http-client/src/decorators/scope-cache.ts +++ b/packages/http-client/src/decorators/scope-cache.ts @@ -1,4 +1,4 @@ -import { Program } from "@typespec/compiler"; +// import { Program } from "@typespec/compiler"; export interface EmitterFilter { includedEmitters: string[]; @@ -11,44 +11,48 @@ export interface ScopedValue { value: T; } -const clientStateMap = new WeakMap>>>(); +// const clientStateMap = new Map>>>(); -export function useClientStateMap( - symbol: symbol, -): [ - get: (program: Program, key: K) => ScopedValue | undefined, - set: (program: Program, key: K, value: ScopedValue) => Map>, - entries: (program: Program) => IterableIterator<[K, ScopedValue]>, -] { - type InnerMap = Map>; - // Ensure we have a map for this program & symbol - function getInnerMap(program: Program): InnerMap { - let stateMap = clientStateMap.get(program); - if (!stateMap) { - stateMap = new Map(); - clientStateMap.set(program, stateMap); - } +// const x = new Set(); - if (!stateMap.has(symbol)) { - stateMap.set(symbol, new Map>()); - } - return stateMap.get(symbol)!; - } +// export function useClientStateMap( +// symbol: symbol, +// ): [ +// get: (program: Program, key: K) => ScopedValue | undefined, +// set: (program: Program, key: K, value: ScopedValue) => Map>, +// entries: (program: Program) => IterableIterator<[K, ScopedValue]>, +// ] { +// type InnerMap = Map>; +// // Ensure we have a map for this program & symbol +// function getInnerMap(program: Program): InnerMap { +// x.add(program); - const get = (program: Program, key: K): ScopedValue | undefined => { - return getInnerMap(program).get(key); - }; +// let stateMap = clientStateMap.get(program); +// if (!stateMap) { +// stateMap = new Map(); +// clientStateMap.set(program, stateMap); +// } - const set = (program: Program, key: K, value: ScopedValue): Map> => { - return getInnerMap(program).set(key, value); - }; +// if (!stateMap.has(symbol)) { +// stateMap.set(symbol, new Map>()); +// } +// return stateMap.get(symbol)!; +// } - const entries = (program: Program): IterableIterator<[K, ScopedValue]> => { - return getInnerMap(program).entries(); - }; +// const get = (program: Program, key: K): ScopedValue | undefined => { +// return getInnerMap(program).get(key); +// }; - return [get, set, entries]; -} +// const set = (program: Program, key: K, value: ScopedValue): Map> => { +// return getInnerMap(program).set(key, value); +// }; + +// const entries = (program: Program): IterableIterator<[K, ScopedValue]> => { +// return getInnerMap(program).entries(); +// }; + +// return [get, set, entries]; +// } export function parseScopeFilter(string: string | undefined): EmitterFilter { if (!string) { diff --git a/packages/http-client/src/index.ts b/packages/http-client/src/index.ts index e15a10b7fef..8b8e4eaa278 100644 --- a/packages/http-client/src/index.ts +++ b/packages/http-client/src/index.ts @@ -1,4 +1,7 @@ +export const namespace = "TypeSpec.OpenAPI"; export * from "./context/index.js"; +export { $featureLifecycle } from "./decorators/feature-lifecycle.js"; export type * from "./interfaces.js"; export { $lib } from "./lib.js"; +export { $decorators } from "./tsp-index.js"; export * from "./utils/index.js"; diff --git a/packages/http-client/src/testing/index.ts b/packages/http-client/src/testing/index.ts index a21748a39c8..405edb9f07e 100644 --- a/packages/http-client/src/testing/index.ts +++ b/packages/http-client/src/testing/index.ts @@ -1,8 +1,10 @@ -import { resolvePath } from "@typespec/compiler"; -import { createTestLibrary, TypeSpecTestLibrary } from "@typespec/compiler/testing"; -import { fileURLToPath } from "url"; +import { + TypeSpecTestLibrary, + createTestLibrary, + findTestPackageRoot, +} from "@typespec/compiler/testing"; -export const TypespecHttpClientLibraryTestLibrary: TypeSpecTestLibrary = createTestLibrary({ - name: "@typespec/http-client-library", - packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../../"), +export const HttpClientTestLibrary: TypeSpecTestLibrary = createTestLibrary({ + name: "@typespec/http-client", + packageRoot: await findTestPackageRoot(import.meta.url), }); diff --git a/packages/http-client/src/tsp-index.ts b/packages/http-client/src/tsp-index.ts index bc46f5954b6..f8e9b4078ea 100644 --- a/packages/http-client/src/tsp-index.ts +++ b/packages/http-client/src/tsp-index.ts @@ -5,5 +5,5 @@ import { $featureLifecycle } from "./decorators/index.js"; export const $decorators = { "TypeSpec.HttpClient": { featureLifecycle: $featureLifecycle, - } as TypeSpecHttpClientDecorators, + } satisfies TypeSpecHttpClientDecorators, }; diff --git a/packages/http-client/src/types/typespec-augmentations.d.ts b/packages/http-client/src/types/typespec-augmentations.d.ts index 33bf69f284d..a948d9c9928 100644 --- a/packages/http-client/src/types/typespec-augmentations.d.ts +++ b/packages/http-client/src/types/typespec-augmentations.d.ts @@ -1,5 +1,5 @@ import { HttpAuth } from "@typespec/http"; -import { authSchemeSymbol, credentialSymbol } from "./credential-symbol.ts"; +import { authSchemeSymbol, credentialSymbol } from "./credential-symbol.js"; declare module "@typespec/compiler" { interface ModelProperty { diff --git a/packages/http-client/test/feature-lifecycle.test.ts b/packages/http-client/test/feature-lifecycle.test.ts index ecfb9d0cc21..61589b9cc84 100644 --- a/packages/http-client/test/feature-lifecycle.test.ts +++ b/packages/http-client/test/feature-lifecycle.test.ts @@ -1,9 +1,11 @@ import { t } from "@typespec/compiler/testing"; +import { $ } from "@typespec/compiler/typekit"; import { expect, it } from "vitest"; +import "../src/typekit/index.js"; import { Tester } from "./test-host.js"; it("should get the feature lifecycle for a model property", async () => { - const { betaProp } = await Tester.compile(t.code` + const { betaProp, program } = await Tester.compile(t.code` namespace Test; model MyModel { @@ -13,5 +15,123 @@ it("should get the feature lifecycle for a model property", async () => { } `); - expect(betaProp.kind).toBe("modelProperty"); + const featureLifecycle = $(program).client.getFeatureLifecycle(betaProp); + expect(featureLifecycle).toBe("Experimental"); +}); + +it("should get the feature lifecycle for a model property within scope", async () => { + const { betaProp, program } = await Tester.compile(t.code` + namespace Test; + + model MyModel { + id: string; + @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "myEmitter"}) + ${t.modelProperty("betaProp")}: string; + } + `); + + const featureLifecycle = $(program).client.getFeatureLifecycle(betaProp, { + emitterName: "myEmitter", + }); + expect(featureLifecycle).toBe("Experimental"); +}); + +it("should get the feature lifecycle for a model property within scope (multiple scopes)", async () => { + const { betaProp, program } = await Tester.compile(t.code` + namespace Test; + + model MyModel { + id: string; + @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "myEmitter, otherEmitter"}) + ${t.modelProperty("betaProp")}: string; + } + `); + + const featureLifecycle = $(program).client.getFeatureLifecycle(betaProp, { + emitterName: "myEmitter", + }); + expect(featureLifecycle).toBe("Experimental"); +}); + +it("should get the feature lifecycle for a model property with scope and unscoped decorator", async () => { + const { betaProp, program } = await Tester.compile(t.code` + namespace Test; + + model MyModel { + id: string; + @featureLifecycle(FeatureLifecycle.Experimental) + ${t.modelProperty("betaProp")}: string; + } + `); + + const featureLifecycle = $(program).client.getFeatureLifecycle(betaProp, { + emitterName: "myEmitter", + }); + expect(featureLifecycle).toBe("Experimental"); +}); + +it("should not get featureLifecycle when not in scope", async () => { + const { betaProp, program } = await Tester.compile(t.code` + namespace Test; + + model MyModel { + id: string; + @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "notMyEmitter"}) + ${t.modelProperty("betaProp")}: string; + } + `); + + const featureLifecycle = $(program).client.getFeatureLifecycle(betaProp, { + emitterName: "myEmitter", + }); + expect(featureLifecycle).toBeUndefined(); +}); + +it("should not get featureLifecycle when no scope passed to query", async () => { + const { betaProp, program } = await Tester.compile(t.code` + namespace Test; + + model MyModel { + id: string; + @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "notMyEmitter"}) + ${t.modelProperty("betaProp")}: string; + } + `); + + const featureLifecycle = $(program).client.getFeatureLifecycle(betaProp); + expect(featureLifecycle).toBeUndefined(); +}); + +it("should get featureLifecycle when not in excluded scopes", async () => { + const { betaProp, program } = await Tester.compile(t.code` + namespace Test; + + model MyModel { + id: string; + @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "!notMyEmitter"}) + ${t.modelProperty("betaProp")}: string; + } + `); + + const featureLifecycle = $(program).client.getFeatureLifecycle(betaProp, { + emitterName: "myEmitter", + }); + expect(featureLifecycle).toBe("Experimental"); +}); + +it("should not get featureLifecycle when in excluded scopes", async () => { + const { betaProp, program } = await Tester.compile(t.code` + namespace Test; + + model MyModel { + id: string; + @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "!myEmitter"}) + ${t.modelProperty("betaProp")}: string; + } + `); + + const featureLifecycle = $(program).client.getFeatureLifecycle(betaProp, { + emitterName: "myEmitter", + }); + expect(featureLifecycle).toBeUndefined(); }); diff --git a/packages/http-client/test/test-host.ts b/packages/http-client/test/test-host.ts index 101b2a6541e..3f67d554d90 100644 --- a/packages/http-client/test/test-host.ts +++ b/packages/http-client/test/test-host.ts @@ -1,16 +1,17 @@ import { resolvePath } from "@typespec/compiler"; import { createTestHost, createTestWrapper, createTester } from "@typespec/compiler/testing"; import { HttpTestLibrary } from "@typespec/http/testing"; +import { HttpClientTestLibrary } from "../src/testing/index.js"; export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { libraries: ["@typespec/http", "@typespec/http-client"], }) .importLibraries() - .using("HttpClient"); + .using("TypeSpec.HttpClient"); export async function createTypespecHttpClientLibraryTestHost() { return createTestHost({ - libraries: [HttpTestLibrary], + libraries: [HttpTestLibrary, HttpClientTestLibrary], }); } @@ -18,6 +19,6 @@ export async function createTypespecHttpClientLibraryTestRunner() { const host = await createTypespecHttpClientLibraryTestHost(); return createTestWrapper(host, { - autoUsings: ["TypeSpec.Http"], + autoUsings: ["TypeSpec.Http", "TypeSpec.HttpClient"], }); } From 5f058d30b9b25f4b3345d953650b6a865dfea9a9 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Fri, 1 Aug 2025 14:07:31 -0700 Subject: [PATCH 03/12] remove commented code --- .../http-client/src/decorators/scope-cache.ts | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/packages/http-client/src/decorators/scope-cache.ts b/packages/http-client/src/decorators/scope-cache.ts index 14fd447acb3..704cdfe486f 100644 --- a/packages/http-client/src/decorators/scope-cache.ts +++ b/packages/http-client/src/decorators/scope-cache.ts @@ -1,5 +1,3 @@ -// import { Program } from "@typespec/compiler"; - export interface EmitterFilter { includedEmitters: string[]; excludedEmitters: string[]; @@ -11,49 +9,6 @@ export interface ScopedValue { value: T; } -// const clientStateMap = new Map>>>(); - -// const x = new Set(); - -// export function useClientStateMap( -// symbol: symbol, -// ): [ -// get: (program: Program, key: K) => ScopedValue | undefined, -// set: (program: Program, key: K, value: ScopedValue) => Map>, -// entries: (program: Program) => IterableIterator<[K, ScopedValue]>, -// ] { -// type InnerMap = Map>; -// // Ensure we have a map for this program & symbol -// function getInnerMap(program: Program): InnerMap { -// x.add(program); - -// let stateMap = clientStateMap.get(program); -// if (!stateMap) { -// stateMap = new Map(); -// clientStateMap.set(program, stateMap); -// } - -// if (!stateMap.has(symbol)) { -// stateMap.set(symbol, new Map>()); -// } -// return stateMap.get(symbol)!; -// } - -// const get = (program: Program, key: K): ScopedValue | undefined => { -// return getInnerMap(program).get(key); -// }; - -// const set = (program: Program, key: K, value: ScopedValue): Map> => { -// return getInnerMap(program).set(key, value); -// }; - -// const entries = (program: Program): IterableIterator<[K, ScopedValue]> => { -// return getInnerMap(program).entries(); -// }; - -// return [get, set, entries]; -// } - export function parseScopeFilter(string: string | undefined): EmitterFilter { if (!string) { return { From 87ec8324cfe02c2ca7004ae95966128e9a31d496 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Fri, 1 Aug 2025 14:16:18 -0700 Subject: [PATCH 04/12] Add change info --- .../joheredi-feature-lifecycle-2025-7-1-14-16-12.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/joheredi-feature-lifecycle-2025-7-1-14-16-12.md diff --git a/.chronus/changes/joheredi-feature-lifecycle-2025-7-1-14-16-12.md b/.chronus/changes/joheredi-feature-lifecycle-2025-7-1-14-16-12.md new file mode 100644 index 00000000000..47de921a1bd --- /dev/null +++ b/.chronus/changes/joheredi-feature-lifecycle-2025-7-1-14-16-12.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http-client" +--- + +Introduce @featureLifecycle decorator \ No newline at end of file From aadeb527421f8f3c7fb8289130d62185f454b759 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Fri, 1 Aug 2025 14:44:38 -0700 Subject: [PATCH 05/12] use unknown for Type --- .../generated-defs/TypeSpec.HttpClient.ts | 26 ++----------------- packages/http-client/lib/decorators.tsp | 12 +-------- 2 files changed, 3 insertions(+), 35 deletions(-) diff --git a/packages/http-client/generated-defs/TypeSpec.HttpClient.ts b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts index 7139c40644e..9c93a65102e 100644 --- a/packages/http-client/generated-defs/TypeSpec.HttpClient.ts +++ b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts @@ -1,16 +1,4 @@ -import type { - DecoratorContext, - Enum, - EnumMember, - Interface, - Model, - ModelProperty, - Namespace, - Operation, - Scalar, - Union, - UnionVariant, -} from "@typespec/compiler"; +import type { DecoratorContext, EnumMember, Type } from "@typespec/compiler"; export interface FeatureLifecycleOptions { readonly emitterScope?: string; @@ -18,17 +6,7 @@ export interface FeatureLifecycleOptions { export type FeatureLifecycleDecorator = ( context: DecoratorContext, - target: - | Model - | ModelProperty - | Operation - | Interface - | Namespace - | Union - | UnionVariant - | Enum - | EnumMember - | Scalar, + target: Type, value: EnumMember, options?: FeatureLifecycleOptions, ) => void; diff --git a/packages/http-client/lib/decorators.tsp b/packages/http-client/lib/decorators.tsp index ecae1f37466..b42e09952cb 100644 --- a/packages/http-client/lib/decorators.tsp +++ b/packages/http-client/lib/decorators.tsp @@ -12,17 +12,7 @@ model FeatureLifecycleOptions { } extern dec featureLifecycle( - target: - | Reflection.Model - | Reflection.ModelProperty - | Reflection.Operation - | Reflection.Interface - | Reflection.Namespace - | Reflection.Union - | Reflection.UnionVariant - | Reflection.Enum - | Reflection.EnumMember - | Reflection.Scalar, + target: unknown, value: Reflection.EnumMember, options?: valueof FeatureLifecycleOptions ); From ae3a2861612ec860da6fecbb3a44e144995df9bb Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Mon, 18 Aug 2025 14:46:47 -0700 Subject: [PATCH 06/12] Update dependencies --- pnpm-lock.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afd177adfe3..32f37b689ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -680,6 +680,12 @@ importers: '@typespec/http': specifier: workspace:^ version: link:../http + '@typespec/library-linter': + specifier: workspace:^ + version: link:../library-linter + '@typespec/tspd': + specifier: workspace:^ + version: link:../tspd eslint: specifier: ^9.23.0 version: 9.32.0 From 9ca3130527a4aec5477df6a01cbb4aeb2766f5c9 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Mon, 18 Aug 2025 14:54:23 -0700 Subject: [PATCH 07/12] Use @experimental decorator --- .../generated-defs/TypeSpec.HttpClient.ts | 7 +++---- packages/http-client/lib/decorators.tsp | 10 +--------- .../src/decorators/feature-lifecycle.ts | 6 +++--- packages/http-client/src/index.ts | 2 +- packages/http-client/src/tsp-index.ts | 4 ++-- .../http-client/test/feature-lifecycle.test.ts | 16 ++++++++-------- 6 files changed, 18 insertions(+), 27 deletions(-) diff --git a/packages/http-client/generated-defs/TypeSpec.HttpClient.ts b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts index 9c93a65102e..d6001ffcbe3 100644 --- a/packages/http-client/generated-defs/TypeSpec.HttpClient.ts +++ b/packages/http-client/generated-defs/TypeSpec.HttpClient.ts @@ -1,16 +1,15 @@ -import type { DecoratorContext, EnumMember, Type } from "@typespec/compiler"; +import type { DecoratorContext, Type } from "@typespec/compiler"; export interface FeatureLifecycleOptions { readonly emitterScope?: string; } -export type FeatureLifecycleDecorator = ( +export type ExperimentalDecorator = ( context: DecoratorContext, target: Type, - value: EnumMember, options?: FeatureLifecycleOptions, ) => void; export type TypeSpecHttpClientDecorators = { - featureLifecycle: FeatureLifecycleDecorator; + experimental: ExperimentalDecorator; }; diff --git a/packages/http-client/lib/decorators.tsp b/packages/http-client/lib/decorators.tsp index b42e09952cb..ae7b01da535 100644 --- a/packages/http-client/lib/decorators.tsp +++ b/packages/http-client/lib/decorators.tsp @@ -3,16 +3,8 @@ import "../dist/src/tsp-index.js"; namespace TypeSpec.HttpClient; -enum FeatureLifecycle { - Experimental, -} - model FeatureLifecycleOptions { ...ClientDecoratorOptions; } -extern dec featureLifecycle( - target: unknown, - value: Reflection.EnumMember, - options?: valueof FeatureLifecycleOptions -); +extern dec experimental(target: unknown, options?: valueof FeatureLifecycleOptions); diff --git a/packages/http-client/src/decorators/feature-lifecycle.ts b/packages/http-client/src/decorators/feature-lifecycle.ts index e78d7aa97c1..f5fc4b5c27f 100644 --- a/packages/http-client/src/decorators/feature-lifecycle.ts +++ b/packages/http-client/src/decorators/feature-lifecycle.ts @@ -1,6 +1,6 @@ import { Program, Type } from "@typespec/compiler"; import { useStateMap } from "@typespec/compiler/utils"; -import { FeatureLifecycleDecorator } from "../../generated-defs/TypeSpec.HttpClient.js"; +import { ExperimentalDecorator } from "../../generated-defs/TypeSpec.HttpClient.js"; import { createStateSymbol } from "../lib.js"; import { parseScopeFilter, ScopedValue } from "./scope-cache.js"; @@ -10,11 +10,11 @@ const [getFeatureLifecycleState, setFeatureLifecycleState] = useStateMap { +export const $experimental: ExperimentalDecorator = (context, target, options) => { const scopeFilter = parseScopeFilter(options?.emitterScope); setFeatureLifecycleState(context.program, target, { emitterFilter: scopeFilter, - value: (value.value && String(value.value)) || value.name, + value: "Experimental", }); }; diff --git a/packages/http-client/src/index.ts b/packages/http-client/src/index.ts index 8b8e4eaa278..4c60bf90fea 100644 --- a/packages/http-client/src/index.ts +++ b/packages/http-client/src/index.ts @@ -1,6 +1,6 @@ export const namespace = "TypeSpec.OpenAPI"; export * from "./context/index.js"; -export { $featureLifecycle } from "./decorators/feature-lifecycle.js"; +export { $experimental } from "./decorators/feature-lifecycle.js"; export type * from "./interfaces.js"; export { $lib } from "./lib.js"; export { $decorators } from "./tsp-index.js"; diff --git a/packages/http-client/src/tsp-index.ts b/packages/http-client/src/tsp-index.ts index f8e9b4078ea..882a9d3ddbb 100644 --- a/packages/http-client/src/tsp-index.ts +++ b/packages/http-client/src/tsp-index.ts @@ -1,9 +1,9 @@ import { TypeSpecHttpClientDecorators } from "../generated-defs/TypeSpec.HttpClient.js"; -import { $featureLifecycle } from "./decorators/index.js"; +import { $experimental } from "./decorators/index.js"; export const $decorators = { "TypeSpec.HttpClient": { - featureLifecycle: $featureLifecycle, + experimental: $experimental, } satisfies TypeSpecHttpClientDecorators, }; diff --git a/packages/http-client/test/feature-lifecycle.test.ts b/packages/http-client/test/feature-lifecycle.test.ts index 61589b9cc84..16df922563f 100644 --- a/packages/http-client/test/feature-lifecycle.test.ts +++ b/packages/http-client/test/feature-lifecycle.test.ts @@ -10,7 +10,7 @@ it("should get the feature lifecycle for a model property", async () => { model MyModel { id: string; - @featureLifecycle(FeatureLifecycle.Experimental) + @experimental ${t.modelProperty("betaProp")}: string; } `); @@ -25,7 +25,7 @@ it("should get the feature lifecycle for a model property within scope", async ( model MyModel { id: string; - @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "myEmitter"}) + @experimental(#{emitterScope: "myEmitter"}) ${t.modelProperty("betaProp")}: string; } `); @@ -42,7 +42,7 @@ it("should get the feature lifecycle for a model property within scope (multiple model MyModel { id: string; - @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "myEmitter, otherEmitter"}) + @experimental(#{emitterScope: "myEmitter, otherEmitter"}) ${t.modelProperty("betaProp")}: string; } `); @@ -59,7 +59,7 @@ it("should get the feature lifecycle for a model property with scope and unscope model MyModel { id: string; - @featureLifecycle(FeatureLifecycle.Experimental) + @experimental ${t.modelProperty("betaProp")}: string; } `); @@ -76,7 +76,7 @@ it("should not get featureLifecycle when not in scope", async () => { model MyModel { id: string; - @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "notMyEmitter"}) + @experimental(#{emitterScope: "notMyEmitter"}) ${t.modelProperty("betaProp")}: string; } `); @@ -93,7 +93,7 @@ it("should not get featureLifecycle when no scope passed to query", async () => model MyModel { id: string; - @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "notMyEmitter"}) + @experimental(#{emitterScope: "notMyEmitter"}) ${t.modelProperty("betaProp")}: string; } `); @@ -108,7 +108,7 @@ it("should get featureLifecycle when not in excluded scopes", async () => { model MyModel { id: string; - @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "!notMyEmitter"}) + @experimental(#{emitterScope: "!notMyEmitter"}) ${t.modelProperty("betaProp")}: string; } `); @@ -125,7 +125,7 @@ it("should not get featureLifecycle when in excluded scopes", async () => { model MyModel { id: string; - @featureLifecycle(FeatureLifecycle.Experimental, #{emitterScope: "!myEmitter"}) + @experimental(#{emitterScope: "!myEmitter"}) ${t.modelProperty("betaProp")}: string; } `); From 6c4060123a3d2a171215f7124ddabe79dc9aca5a Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Mon, 18 Aug 2025 15:26:10 -0700 Subject: [PATCH 08/12] update codeowners --- .github/CODEOWNERS | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index afd7b3de6a2..ab54b35cd6b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -36,6 +36,11 @@ cspell.yaml ###################### /packages/http-client-js/ @joheredi @timotheeguerin @timovv @qiaozha @MaryGao @xirzec @bterlson @markcowl @allenjzhang @witemple-msft @chrisradek @AlitzelMendez +###################### +# Client Framework +###################### +/packages/http-client/ @joheredi @timotheeguerin @timovv @qiaozha @MaryGao @xirzec @bterlson @markcowl @allenjzhang @witemple-msft @chrisradek @AlitzelMendez @tadelesh + ###################### # IDE ###################### From 3e28410e0113478ac2107375e91954401fe47912 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Tue, 19 Aug 2025 14:28:09 -0700 Subject: [PATCH 09/12] Address review comments --- ...edi-feature-lifecycle-2025-7-1-14-16-12.md | 2 +- packages/http-client/lib/common.tsp | 4 ++ packages/http-client/src/decorators/common.ts | 0 .../src/decorators/feature-lifecycle.ts | 49 ++++++++++++++----- .../http-client/src/decorators/scope-cache.ts | 6 +-- packages/http-client/src/index.ts | 3 +- .../http-client/src/typekit/kits/client.ts | 10 ++-- .../test/feature-lifecycle.test.ts | 19 ++++++- 8 files changed, 71 insertions(+), 22 deletions(-) delete mode 100644 packages/http-client/src/decorators/common.ts diff --git a/.chronus/changes/joheredi-feature-lifecycle-2025-7-1-14-16-12.md b/.chronus/changes/joheredi-feature-lifecycle-2025-7-1-14-16-12.md index 47de921a1bd..969f6599df3 100644 --- a/.chronus/changes/joheredi-feature-lifecycle-2025-7-1-14-16-12.md +++ b/.chronus/changes/joheredi-feature-lifecycle-2025-7-1-14-16-12.md @@ -4,4 +4,4 @@ packages: - "@typespec/http-client" --- -Introduce @featureLifecycle decorator \ No newline at end of file +Introduces @experimental decorator for annotating generated code as experimental. diff --git a/packages/http-client/lib/common.tsp b/packages/http-client/lib/common.tsp index 13721e82bd3..32e10019630 100644 --- a/packages/http-client/lib/common.tsp +++ b/packages/http-client/lib/common.tsp @@ -1,5 +1,9 @@ namespace TypeSpec.HttpClient; +/** + * Specifies the target emitters that the decorator should apply. If not set, the decorator will be applied to all emitters by default. + * You can use ”!” to exclude specific emitters, for example: `!@typespec/http-client-js, !@typespec/http-client-csharp` + */ model ClientDecoratorOptions { emitterScope?: string; } diff --git a/packages/http-client/src/decorators/common.ts b/packages/http-client/src/decorators/common.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/http-client/src/decorators/feature-lifecycle.ts b/packages/http-client/src/decorators/feature-lifecycle.ts index f5fc4b5c27f..a7e85c4e8e8 100644 --- a/packages/http-client/src/decorators/feature-lifecycle.ts +++ b/packages/http-client/src/decorators/feature-lifecycle.ts @@ -1,4 +1,4 @@ -import { Program, Type } from "@typespec/compiler"; +import { createDiagnosticCollector, DiagnosticResult, Program, Type } from "@typespec/compiler"; import { useStateMap } from "@typespec/compiler/utils"; import { ExperimentalDecorator } from "../../generated-defs/TypeSpec.HttpClient.js"; import { createStateSymbol } from "../lib.js"; @@ -6,12 +6,23 @@ import { parseScopeFilter, ScopedValue } from "./scope-cache.js"; const featureLifecycleStateSymbol = createStateSymbol("featureLifecycleState"); -const [getFeatureLifecycleState, setFeatureLifecycleState] = useStateMap>( - featureLifecycleStateSymbol, -); +export type FeatureLifecycleStage = "Experimental"; + +const [getFeatureLifecycleState, setFeatureLifecycleState] = useStateMap< + Type, + ScopedValue +>(featureLifecycleStateSymbol); export const $experimental: ExperimentalDecorator = (context, target, options) => { const scopeFilter = parseScopeFilter(options?.emitterScope); + if (scopeFilter.excludedEmitters.length > 0 && scopeFilter.includedEmitters.length > 0) { + context.program.reportDiagnostic({ + code: "include-and-exclude-scopes", + message: "The @experimental should only either include or exclude scopes, not both.", + severity: "error", + target, + }); + } setFeatureLifecycleState(context.program, target, { emitterFilter: scopeFilter, value: "Experimental", @@ -25,33 +36,49 @@ export function getClientFeatureLifecycle( program: Program, target: Type, options: GetFeatureLifecycleOptions = {}, -): string | undefined { +): DiagnosticResult { + const diagnostics = createDiagnosticCollector(); + const lifecycle = getFeatureLifecycleState(program, target); if (!lifecycle) { - return undefined; + return diagnostics.wrap(undefined); } const emitterScope = options.emitterName; const lifecycleValue = lifecycle.value; - if (lifecycle.emitterFilter.isUnscoped) { - return lifecycle.value; + if (!lifecycle.emitterFilter.isScoped) { + return diagnostics.wrap(lifecycleValue); } + // Lifecycle is scoped but no emitter scope is provided to the query function so we return undefined + // this is because we can't determine which emitter the lifecycle is associated with. if (!emitterScope) { - return lifecycle?.emitterFilter.isUnscoped ? lifecycleValue : undefined; + diagnostics.add({ + code: "use-client-context-without-provider", + message: "No emitter scope provided to getClientFeatureLifecycle.", + target, + severity: "warning", + }); + return diagnostics.wrap(undefined); } if (lifecycle.emitterFilter.includedEmitters.length) { - return lifecycle.emitterFilter.includedEmitters.includes(emitterScope) + const value = lifecycle.emitterFilter.includedEmitters.includes(emitterScope) ? lifecycleValue : undefined; + + return diagnostics.wrap(value); } if (lifecycle.emitterFilter.excludedEmitters.length) { - return lifecycle.emitterFilter.excludedEmitters.includes(emitterScope) + const value = lifecycle.emitterFilter.excludedEmitters.includes(emitterScope) ? undefined : lifecycleValue; + + return diagnostics.wrap(value); } + + return diagnostics.wrap(undefined); } diff --git a/packages/http-client/src/decorators/scope-cache.ts b/packages/http-client/src/decorators/scope-cache.ts index 704cdfe486f..48c4e97c1b9 100644 --- a/packages/http-client/src/decorators/scope-cache.ts +++ b/packages/http-client/src/decorators/scope-cache.ts @@ -1,7 +1,7 @@ export interface EmitterFilter { includedEmitters: string[]; excludedEmitters: string[]; - isUnscoped: boolean; + isScoped: boolean; } export interface ScopedValue { @@ -14,7 +14,7 @@ export function parseScopeFilter(string: string | undefined): EmitterFilter { return { excludedEmitters: [], includedEmitters: [], - isUnscoped: true, + isScoped: false, }; } @@ -34,6 +34,6 @@ export function parseScopeFilter(string: string | undefined): EmitterFilter { return { excludedEmitters, includedEmitters, - isUnscoped: false, + isScoped: true, }; } diff --git a/packages/http-client/src/index.ts b/packages/http-client/src/index.ts index 4c60bf90fea..d8c0f943aaa 100644 --- a/packages/http-client/src/index.ts +++ b/packages/http-client/src/index.ts @@ -1,6 +1,5 @@ -export const namespace = "TypeSpec.OpenAPI"; +export const namespace = "TypeSpec.HttpClient"; export * from "./context/index.js"; -export { $experimental } from "./decorators/feature-lifecycle.js"; export type * from "./interfaces.js"; export { $lib } from "./lib.js"; export { $decorators } from "./tsp-index.js"; diff --git a/packages/http-client/src/typekit/kits/client.ts b/packages/http-client/src/typekit/kits/client.ts index 8d1c8399f80..4969f274288 100644 --- a/packages/http-client/src/typekit/kits/client.ts +++ b/packages/http-client/src/typekit/kits/client.ts @@ -8,7 +8,7 @@ import { Operation, Type, } from "@typespec/compiler"; -import { defineKit } from "@typespec/compiler/typekit"; +import { createDiagnosable, defineKit, Diagnosable } from "@typespec/compiler/typekit"; import { getHttpService, getServers, @@ -34,7 +34,9 @@ interface ClientKit extends NameKit { * @param type The type to get the feature lifecycle for * @param options The options to use when getting the feature lifecycle */ - getFeatureLifecycle(type: Type, options?: GetFeatureLifecycleOptions): string | undefined; + getFeatureLifecycle: Diagnosable< + (type: Type, options?: GetFeatureLifecycleOptions) => string | undefined + >; /** * Get the parent of a client * @param type The client to get the parent of @@ -109,9 +111,9 @@ export const clientOperationCache = new Map(); defineKit({ client: { - getFeatureLifecycle(type, options) { + getFeatureLifecycle: createDiagnosable(function (type, options) { return getClientFeatureLifecycle(this.program, type, options); - }, + }), getParent(client) { const type = client.kind === "Client" ? client.type : client; if (type.namespace && type.namespace !== this.program.getGlobalNamespaceType()) { diff --git a/packages/http-client/test/feature-lifecycle.test.ts b/packages/http-client/test/feature-lifecycle.test.ts index 16df922563f..a613d2529f3 100644 --- a/packages/http-client/test/feature-lifecycle.test.ts +++ b/packages/http-client/test/feature-lifecycle.test.ts @@ -1,4 +1,4 @@ -import { t } from "@typespec/compiler/testing"; +import { expectDiagnostics, t } from "@typespec/compiler/testing"; import { $ } from "@typespec/compiler/typekit"; import { expect, it } from "vitest"; import "../src/typekit/index.js"; @@ -135,3 +135,20 @@ it("should not get featureLifecycle when in excluded scopes", async () => { }); expect(featureLifecycle).toBeUndefined(); }); + +it("should report diagnostics when both include and exclude are set", async () => { + const diagnostics = await Tester.diagnose(` + namespace Test; + + model MyModel { + id: string; + @experimental(#{emitterScope: "!myEmitter,otherEmitter"}) + betaProp: string; + } + `); + + expectDiagnostics(diagnostics, { + code: "include-and-exclude-scopes", + message: "The @experimental should only either include or exclude scopes, not both.", + }); +}); From 5061a17afb602e5a1f17fa76761c4c349f77b5a8 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Tue, 19 Aug 2025 17:10:09 -0700 Subject: [PATCH 10/12] Use Tester instead of testHost and runner --- packages/http-client/test/test-host.ts | 20 +- .../test/typekit/client-library.test.ts | 43 ++-- .../http-client/test/typekit/client.test.ts | 198 +++++++++--------- .../test/typekit/model-property.test.ts | 86 ++++---- 4 files changed, 156 insertions(+), 191 deletions(-) diff --git a/packages/http-client/test/test-host.ts b/packages/http-client/test/test-host.ts index 3f67d554d90..f92fe56ecde 100644 --- a/packages/http-client/test/test-host.ts +++ b/packages/http-client/test/test-host.ts @@ -1,24 +1,8 @@ import { resolvePath } from "@typespec/compiler"; -import { createTestHost, createTestWrapper, createTester } from "@typespec/compiler/testing"; -import { HttpTestLibrary } from "@typespec/http/testing"; -import { HttpClientTestLibrary } from "../src/testing/index.js"; +import { createTester } from "@typespec/compiler/testing"; export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { libraries: ["@typespec/http", "@typespec/http-client"], }) .importLibraries() - .using("TypeSpec.HttpClient"); - -export async function createTypespecHttpClientLibraryTestHost() { - return createTestHost({ - libraries: [HttpTestLibrary, HttpClientTestLibrary], - }); -} - -export async function createTypespecHttpClientLibraryTestRunner() { - const host = await createTypespecHttpClientLibraryTestHost(); - - return createTestWrapper(host, { - autoUsings: ["TypeSpec.Http", "TypeSpec.HttpClient"], - }); -} + .using("HttpClient", "Http"); diff --git a/packages/http-client/test/typekit/client-library.test.ts b/packages/http-client/test/typekit/client-library.test.ts index ab3ee2ff0d8..6ea0a3622de 100644 --- a/packages/http-client/test/typekit/client-library.test.ts +++ b/packages/http-client/test/typekit/client-library.test.ts @@ -1,25 +1,18 @@ -import type { Namespace } from "@typespec/compiler"; -import type { BasicTestRunner } from "@typespec/compiler/testing"; +import { t } from "@typespec/compiler/testing"; import { $ } from "@typespec/compiler/typekit"; -import { beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import "../../src/typekit/index.js"; -import { createTypespecHttpClientLibraryTestRunner } from "../test-host.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createTypespecHttpClientLibraryTestRunner(); -}); +import { Tester } from "../test-host.js"; describe("listNamespaces", () => { it("basic", async () => { - await runner.compile(` + const { program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) namespace DemoService; `); - const tk = $(runner.program); + const tk = $(program); expect(tk.clientLibrary.listNamespaces()).toHaveLength(1); expect(tk.clientLibrary.listNamespaces()[0].name).toEqual("DemoService"); @@ -27,7 +20,7 @@ describe("listNamespaces", () => { it("nested", async () => { // we only want to return the top level namespaces - await runner.compile(` + const { program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) @@ -38,7 +31,7 @@ describe("listNamespaces", () => { } } `); - const tk = $(runner.program); + const tk = $(program); expect(tk.clientLibrary.listNamespaces()).toHaveLength(1); expect(tk.clientLibrary.listNamespaces()[0].name).toEqual("DemoService"); @@ -55,10 +48,10 @@ describe("listNamespaces", () => { describe("listClients", () => { it("should only get clients for defined namespaces in the spec", async () => { - await runner.compile(` + const { program } = await Tester.compile(t.code` op foo(): void; `); - const tk = $(runner.program); + const tk = $(program); const namespace = tk.program.getGlobalNamespaceType(); const client = tk.client.getClient(namespace); @@ -69,28 +62,28 @@ describe("listClients", () => { }); it("should get the client", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const responses = tk.clientLibrary.listClients(DemoService); expect(responses).toHaveLength(1); expect(responses[0].name).toEqual("DemoServiceClient"); }); it("get subclients", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService { - namespace NestedService {}; + namespace ${t.namespace("DemoService")} { + namespace ${t.namespace("NestedService")} {}; } - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + `); + const tk = $(program); const responses = tk.clientLibrary.listClients(DemoService); expect(responses).toHaveLength(1); diff --git a/packages/http-client/test/typekit/client.test.ts b/packages/http-client/test/typekit/client.test.ts index b46736924b7..3cfa862758d 100644 --- a/packages/http-client/test/typekit/client.test.ts +++ b/packages/http-client/test/typekit/client.test.ts @@ -1,26 +1,20 @@ -import { Interface, Namespace, StringLiteral, StringValue, Union } from "@typespec/compiler"; -import type { BasicTestRunner } from "@typespec/compiler/testing"; +import { StringLiteral, StringValue, Union } from "@typespec/compiler"; +import { t } from "@typespec/compiler/testing"; import { $ } from "@typespec/compiler/typekit"; import { ok } from "assert"; -import { beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import "../../src/typekit/index.js"; -import { createTypespecHttpClientLibraryTestRunner } from "../test-host.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createTypespecHttpClientLibraryTestRunner(); -}); +import { Tester } from "../test-host.js"; describe("isSameConstructor", () => { it("should return true for the same client", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.client.getClient(DemoService); @@ -28,17 +22,17 @@ describe("isSameConstructor", () => { }); it("should return false for the clients with different constructors", async () => { - const { DemoService, SubClient } = (await runner.compile(` + const { DemoService, SubClient, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService { + namespace ${t.namespace("DemoService")} { @useAuth(ApiKeyAuth) - @test namespace SubClient { + namespace ${t.namespace("SubClient")} { } } - `)) as { DemoService: Namespace; SubClient: Namespace }; - const tk = $(runner.program); + `); + const tk = $(program); const client = tk.client.getClient(DemoService); const subClient = tk.client.getClient(SubClient); @@ -47,17 +41,17 @@ describe("isSameConstructor", () => { }); it.skip("should return true when subclient doesn't override the client params", async () => { - const { DemoService, SubClient } = (await runner.compile(` + const { DemoService, SubClient, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) @useAuth(ApiKeyAuth) - @test namespace DemoService { - @test namespace SubClient { + namespace ${t.namespace("DemoService")} { + namespace ${t.namespace("SubClient")} { } } - `)) as { DemoService: Namespace; SubClient: Namespace }; - const tk = $(runner.program); + `); + const tk = $(program); const demoClient = tk.client.getClient(DemoService); const subClient = tk.client.getClient(SubClient); @@ -66,16 +60,16 @@ describe("isSameConstructor", () => { }); it("should return false for the clients with different constructor", async () => { - const { DemoService, SubClient } = (await runner.compile(` + const { DemoService, SubClient, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService { - @test namespace SubClient { + namespace ${t.namespace("DemoService")} { + @test namespace ${t.namespace("SubClient")} { } } - `)) as { DemoService: Namespace; SubClient: Namespace }; - const tk = $(runner.program); + `); + const tk = $(program); const client = tk.client.getClient(DemoService); const subClient = tk.client.getClient(SubClient); @@ -86,10 +80,10 @@ describe("isSameConstructor", () => { describe("getClient", () => { it("should get a client from the globalNamespace", async () => { - (await runner.compile(` + const { program } = await Tester.compile(t.code` op foo(): void; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + `); + const tk = $(program); const namespace = tk.program.getGlobalNamespaceType(); const client = tk.client.getClient(namespace); @@ -98,13 +92,13 @@ describe("getClient", () => { }); it("should get the client", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.client.getClient(DemoService); @@ -114,13 +108,13 @@ describe("getClient", () => { }); it("should preserve client object identity", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client1 = tk.client.getClient(DemoService); const client2 = tk.client.getClient(DemoService); @@ -128,21 +122,21 @@ describe("getClient", () => { }); it("should get a flattened list of clients", async () => { - const { DemoService, BarBaz } = (await runner.compile(` + const { DemoService, BarBaz, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService { - @test namespace Foo { - @test namespace FooBaz {} + namespace ${t.namespace("DemoService")} { + namespace Foo { + namespace FooBaz {} } @test namespace Bar { - @test interface BarBaz {} + interface ${t.interface("BarBaz")} {} } } - `)) as { DemoService: Namespace; BarBaz: Interface }; - const tk = $(runner.program); + `); + const tk = $(program); const client = tk.client.getClient(DemoService); const flatClients = tk.client.flat(client); @@ -156,13 +150,13 @@ describe("getClient", () => { describe("getConstructor", () => { describe("credential parameter", () => { it("none", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -174,14 +168,14 @@ describe("getConstructor", () => { expect(tk.scalar.isString(params[0].type)).toBeTruthy(); }); it("apikey", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) @useAuth(ApiKeyAuth) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -204,7 +198,7 @@ describe("getConstructor", () => { * - A single constructor with an endpoint parameter that is required * - A single constructor with a credential parameter that is required. */ - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) @@ -213,9 +207,9 @@ describe("getConstructor", () => { authorizationUrl: "https://login.microsoftonline.com/common/oauth2/authorize"; scopes: ["https://security.microsoft.com/.default"]; }]>) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -233,13 +227,13 @@ describe("getConstructor", () => { }); describe("endpoint", () => { it("no servers", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -251,14 +245,14 @@ describe("getConstructor", () => { expect(tk.scalar.isString(params[0].type)).toBeTruthy(); }); it("one server, no params", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @server("https://example.com", "The service endpoint") @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -278,14 +272,14 @@ describe("getConstructor", () => { * - The endpoint default value is the url template https://example.com/{name}/foo * - There is a required name parameter of type string */ - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @server("https://example.com/{name}/foo", "My service url", { name: string }) @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -321,14 +315,14 @@ describe("getConstructor", () => { * - A constructor parameter named endpoint which maps to the template variable that is required but has a default value * - A constructor parameter named _endpoint (due to collission) which has a default value which is the url template https://{endpoint}/foo */ - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @server("https://{endpoint}/foo", "My service url", { endpoint: string }) @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -362,15 +356,15 @@ describe("getConstructor", () => { * - The endpoint parameter has a type of union including the 2 clientDefaultValues plus string. */ - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @server("https://example.com", "The service endpoint") @server("https://example.org", "The service endpoint") @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; // There is a single constructor so no overloads. @@ -399,13 +393,13 @@ describe("getConstructor", () => { describe("isPubliclyInitializable", () => { it("namespace", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const responses = tk.clientLibrary.listClients(DemoService); expect(responses).toHaveLength(1); @@ -413,15 +407,15 @@ describe("isPubliclyInitializable", () => { expect(tk.client.isPubliclyInitializable(responses[0])).toBeTruthy(); }); it("nested namespace", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService { + namespace ${t.namespace("DemoService")} { namespace NestedService {}; } - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + `); + const tk = $(program); const responses = tk.clientLibrary.listClients(DemoService); expect(responses).toHaveLength(1); @@ -434,15 +428,15 @@ describe("isPubliclyInitializable", () => { expect(tk.client.isPubliclyInitializable(subclients[0])).toBeTruthy(); }); it("nested interface", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService { + namespace ${t.namespace("DemoService")} { interface NestedInterface {}; } - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + `); + const tk = $(program); const responses = tk.clientLibrary.listClients(DemoService); expect(responses).toHaveLength(1); @@ -458,10 +452,10 @@ describe("isPubliclyInitializable", () => { describe("listServiceOperations", () => { it("should list only operations defined in the spec", async () => { - await runner.compile(` + const { program } = await Tester.compile(t.code` op foo(): void; `); - const tk = $(runner.program); + const tk = $(program); const namespace = tk.program.getGlobalNamespaceType(); const client = tk.client.getClient(namespace); @@ -473,32 +467,32 @@ describe("listServiceOperations", () => { }); it("no operations", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const operations = tk.client.listHttpOperations(client); expect(operations).toHaveLength(0); }); it("nested namespace", async () => { - const { DemoService, NestedService } = (await runner.compile(` + const { DemoService, NestedService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService { + namespace ${t.namespace("DemoService")} { @route("demo") op demoServiceOp(): void; - @test namespace NestedService { + namespace ${t.namespace("NestedService")} { @route("nested") op nestedServiceOp(): void; }; } - `)) as { DemoService: Namespace; NestedService: Namespace }; - const tk = $(runner.program); + `); + const tk = $(program); const demoServiceClient = tk.clientLibrary.listClients(DemoService)[0]; expect(tk.client.listHttpOperations(demoServiceClient)).toHaveLength(1); diff --git a/packages/http-client/test/typekit/model-property.test.ts b/packages/http-client/test/typekit/model-property.test.ts index 0237bb07827..5fa4824cc93 100644 --- a/packages/http-client/test/typekit/model-property.test.ts +++ b/packages/http-client/test/typekit/model-property.test.ts @@ -1,27 +1,21 @@ -import { Namespace, StringValue } from "@typespec/compiler"; -import type { BasicTestRunner } from "@typespec/compiler/testing"; +import { StringValue } from "@typespec/compiler"; +import { t } from "@typespec/compiler/testing"; import { $ } from "@typespec/compiler/typekit"; import { ok } from "assert"; -import { beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import "../../src/typekit/index.js"; -import { createTypespecHttpClientLibraryTestRunner } from "../test-host.js"; - -let runner: BasicTestRunner; - -beforeEach(async () => { - runner = await createTypespecHttpClientLibraryTestRunner(); -}); +import { Tester } from "../test-host.js"; describe("getCredentialAuth", () => { it("should return the correct http scheme", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) @useAuth(ApiKeyAuth) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.client.getClient(DemoService); const constructor = tk.client.getConstructor(client); @@ -37,7 +31,7 @@ describe("getCredentialAuth", () => { }); it("should return the correct http schemes", async () => { - const { DemoService } = (await runner.compile(` + const { program, DemoService } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) @@ -46,9 +40,9 @@ describe("getCredentialAuth", () => { authorizationUrl: "https://login.microsoftonline.com/common/oauth2/authorize"; scopes: ["https://security.microsoft.com/.default"]; }]>) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.client.getClient(DemoService); const constructor = tk.client.getConstructor(client); @@ -68,13 +62,13 @@ describe("getCredentialAuth", () => { describe("isOnClient", () => { describe("endpoint", () => { it("no servers", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -84,14 +78,14 @@ describe("isOnClient", () => { expect(tk.modelProperty.isOnClient(client, params[0])).toBe(true); }); it("one server, no params", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @server("https://example.com", "The service endpoint") @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -101,14 +95,14 @@ describe("isOnClient", () => { expect(tk.modelProperty.isOnClient(client, params[0])).toBe(true); }); it("one server with parameter", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @server("https://example.com/{name}/foo", "My service url", { name: string }) @service(#{ title: "Widget Service", }) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -132,14 +126,14 @@ describe("isOnClient", () => { }); describe("credential", () => { it("apikey", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) @useAuth(ApiKeyAuth) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.client.getClient(DemoService); const constructor = tk.client.getConstructor(client); @@ -151,7 +145,7 @@ describe("isOnClient", () => { expect(tk.modelProperty.isOnClient(client, credential)).toBe(true); }); it("bearer", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) @@ -160,9 +154,9 @@ describe("isOnClient", () => { authorizationUrl: "https://login.microsoftonline.com/common/oauth2/authorize"; scopes: ["https://security.microsoft.com/.default"]; }]>) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -179,14 +173,14 @@ describe("isOnClient", () => { describe("isCredential", () => { it("apikey", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) @useAuth(ApiKeyAuth) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); @@ -198,7 +192,7 @@ describe("isCredential", () => { expect(tk.modelProperty.isCredential(credential)).toBe(true); }); it("bearer", async () => { - const { DemoService } = (await runner.compile(` + const { DemoService, program } = await Tester.compile(t.code` @service(#{ title: "Widget Service", }) @@ -207,9 +201,9 @@ describe("isCredential", () => { authorizationUrl: "https://login.microsoftonline.com/common/oauth2/authorize"; scopes: ["https://security.microsoft.com/.default"]; }]>) - @test namespace DemoService; - `)) as { DemoService: Namespace }; - const tk = $(runner.program); + namespace ${t.namespace("DemoService")}; + `); + const tk = $(program); const client = tk.clientLibrary.listClients(DemoService)[0]; const constructor = tk.client.getConstructor(client); From dc1434b7629927477c77c8c2aab6c39a16b8c84a Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Wed, 20 Aug 2025 12:53:51 -0700 Subject: [PATCH 11/12] Increase timeout --- .../src/typescript/components/type-expression.tsx | 2 +- packages/http-client/vitest.config.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/emitter-framework/src/typescript/components/type-expression.tsx b/packages/emitter-framework/src/typescript/components/type-expression.tsx index 364710ec5aa..63dd7719c90 100644 --- a/packages/emitter-framework/src/typescript/components/type-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/type-expression.tsx @@ -1,8 +1,8 @@ +import { Experimental_OverridableComponent } from "#core/index.js"; import { For } from "@alloy-js/core"; import { Reference, ValueExpression } from "@alloy-js/typescript"; import type { IntrinsicType, Model, Scalar, Type } from "@typespec/compiler"; import type { Typekit } from "@typespec/compiler/typekit"; -import { Experimental_OverridableComponent } from "../../core/components/overrides/component-overrides.jsx"; import { useTsp } from "../../core/context/tsp-context.js"; import { reportTypescriptDiagnostic } from "../../typescript/lib.js"; import { efRefkey } from "../utils/refkey.js"; diff --git a/packages/http-client/vitest.config.ts b/packages/http-client/vitest.config.ts index 0221876ef0e..16a593472d0 100644 --- a/packages/http-client/vitest.config.ts +++ b/packages/http-client/vitest.config.ts @@ -8,6 +8,7 @@ export default mergeConfig( test: { include: ["test/**/*.test.ts"], passWithNoTests: true, + testTimeout: 10_000, }, esbuild: { jsx: "preserve", From 773d79fad00d728ac255f59f535f93651648a620 Mon Sep 17 00:00:00 2001 From: Jose Manuel Heredia Hidalgo Date: Wed, 20 Aug 2025 12:57:29 -0700 Subject: [PATCH 12/12] refert ef changes --- .../src/typescript/components/type-expression.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/emitter-framework/src/typescript/components/type-expression.tsx b/packages/emitter-framework/src/typescript/components/type-expression.tsx index 63dd7719c90..364710ec5aa 100644 --- a/packages/emitter-framework/src/typescript/components/type-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/type-expression.tsx @@ -1,8 +1,8 @@ -import { Experimental_OverridableComponent } from "#core/index.js"; import { For } from "@alloy-js/core"; import { Reference, ValueExpression } from "@alloy-js/typescript"; import type { IntrinsicType, Model, Scalar, Type } from "@typespec/compiler"; import type { Typekit } from "@typespec/compiler/typekit"; +import { Experimental_OverridableComponent } from "../../core/components/overrides/component-overrides.jsx"; import { useTsp } from "../../core/context/tsp-context.js"; import { reportTypescriptDiagnostic } from "../../typescript/lib.js"; import { efRefkey } from "../utils/refkey.js";