From d648b7fa102048033fea9515c0bcb8fd3dde697e Mon Sep 17 00:00:00 2001 From: johnpyp Date: Sun, 28 Dec 2025 17:53:15 -0500 Subject: [PATCH] http-client-js: improve default output formatting some basic, easy things like removing double-semicolons, consistent newlines, consistent semicolon formatting for class members. also added a `decent_formatting.md` scenario for this --- .../src/testing/scenario-test/harness.ts | 13 ++- .../client-context/client-context-factory.tsx | 1 + .../client-context/client-context.tsx | 10 +- .../client-context/parametrized-endpoint.tsx | 2 +- .../http-client-js/src/components/client.tsx | 20 ++-- .../scenarios/client/decent_formatting.md | 93 +++++++++++++++++++ 6 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 packages/http-client-js/test/scenarios/client/decent_formatting.md diff --git a/packages/emitter-framework/src/testing/scenario-test/harness.ts b/packages/emitter-framework/src/testing/scenario-test/harness.ts index 0d649da6d43..3ee2f7d22fa 100644 --- a/packages/emitter-framework/src/testing/scenario-test/harness.ts +++ b/packages/emitter-framework/src/testing/scenario-test/harness.ts @@ -172,6 +172,7 @@ function describeScenarios( for (const scenario of scenarioFile.scenarios) { const isOnly = scenario.title.includes("only:"); const isSkip = scenario.title.includes("skip:"); + const isNoFormat = scenario.title.includes("no-format"); const describeFn = isSkip ? describe.skip : isOnly ? describe.only : describe; let outputFiles: Record; @@ -199,14 +200,20 @@ function describeScenarios( if (SCENARIOS_UPDATE) { try { - testBlock.content = await languageConfiguration.format(result); + testBlock.content = isNoFormat + ? result + : await languageConfiguration.format(result); } catch { // If formatting fails, we still want to update the content testBlock.content = result; } } else { - const expected = await languageConfiguration.format(testBlock.content); - const actual = await languageConfiguration.format(result); + const expected = isNoFormat + ? testBlock.content + : await languageConfiguration.format(testBlock.content); + const actual = isNoFormat + ? result + : await languageConfiguration.format(result); expect(actual).toBe(expected); } }); diff --git a/packages/http-client-js/src/components/client-context/client-context-factory.tsx b/packages/http-client-js/src/components/client-context/client-context-factory.tsx index 697cd1af257..dd3bb40b0af 100644 --- a/packages/http-client-js/src/components/client-context/client-context-factory.tsx +++ b/packages/http-client-js/src/components/client-context/client-context-factory.tsx @@ -63,6 +63,7 @@ export function ClientContextFactoryDeclaration(props: ClientContextFactoryProps parameters={parameters} > {resolvedEndpoint} + return ); diff --git a/packages/http-client-js/src/components/client-context/client-context.tsx b/packages/http-client-js/src/components/client-context/client-context.tsx index 1647c91cd5b..22962f04ed9 100644 --- a/packages/http-client-js/src/components/client-context/client-context.tsx +++ b/packages/http-client-js/src/components/client-context/client-context.tsx @@ -1,4 +1,4 @@ -import { type Children } from "@alloy-js/core"; +import { type Children, List } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import * as cl from "@typespec/http-client"; import { ClientContextDeclaration } from "./client-context-declaration.jsx"; @@ -15,9 +15,11 @@ export function ClientContext(props: ClientContextProps) { const fileName = namePolicy.getName(props.client.name + "Context", "variable"); return ( - - - + + + + + ); } diff --git a/packages/http-client-js/src/components/client-context/parametrized-endpoint.tsx b/packages/http-client-js/src/components/client-context/parametrized-endpoint.tsx index 1347e008505..c2f02c1bcd7 100644 --- a/packages/http-client-js/src/components/client-context/parametrized-endpoint.tsx +++ b/packages/http-client-js/src/components/client-context/parametrized-endpoint.tsx @@ -36,7 +36,7 @@ export function ParametrizedEndpoint(props: ParametrizedEndpointProps) { {code` "${props.template}".replace(/{([^}]+)}/g, (_, key) => key in ${paramsRef} ? String(params[key]) : (() => { throw new Error(\`Missing parameter: $\{key}\`); })() - ); + ) `} ); diff --git a/packages/http-client-js/src/components/client.tsx b/packages/http-client-js/src/components/client.tsx index 3c3beceee37..ae9262bff1c 100644 --- a/packages/http-client-js/src/components/client.tsx +++ b/packages/http-client-js/src/components/client.tsx @@ -57,13 +57,15 @@ export function ClientClass(props: ClientClassProps) { return ( - - + <> + ; + + {(subClient) => } @@ -138,13 +140,13 @@ function ClientConstructor(props: ClientConstructorProps) { {clientContextFieldRef} ={" "} ;
- + {(subClient) => { const subClientFieldRef = getSubClientClassFieldRef(subClient); const subClientArgs = calculateSubClientArgs(subClient, constructorParameters); return ( <> - {subClientFieldRef} = ; + {subClientFieldRef} = ); }} diff --git a/packages/http-client-js/test/scenarios/client/decent_formatting.md b/packages/http-client-js/test/scenarios/client/decent_formatting.md new file mode 100644 index 00000000000..49281b8fb72 --- /dev/null +++ b/packages/http-client-js/test/scenarios/client/decent_formatting.md @@ -0,0 +1,93 @@ +# Should generate a client with consistent and normal Typescript code no-format + +This test verifies that a basic service with multiple routes results in "decently" formatted output. + +The goal isn't perfect formatting, just some level of consistency for basic usecases. Prettier or other formatters should be run +to achieve actually good formatting. + +- no double semicolons (constructor lines, createWidgetsClientContext, for example) +- the constructor properly separates sub-client assignments with newlines. +- each type declaration in the DemoServiceClient class has semicolons at the end, consistently. +- newlines separate `ClientContext` blocks + +## TypeSpec + +```tsp +@service(#{ title: "Multi Route Service" }) +namespace DemoService; + +model Widget { + id: string; + weight: int32; +} + +model User { + id: string; + name: string; +} + +@error +model Error { + code: int32; + message: string; +} + +@route("/widgets") +@tag("Widgets") +interface Widgets { + @get list(): Widget[] | Error; + @get read(@path id: string): Widget | Error; +} + +@route("/users") +@tag("Users") +interface Users { + @get list(): User[] | Error; + @get read(@path id: string): User | Error; +} +``` + +## TypeScript + +### Client + +It generates a root client with multiple sub-clients, each properly separated by newlines in the constructor. + +```ts src/demoServiceClient.ts class DemoServiceClient +export class DemoServiceClient { + #context: DemoServiceClientContext; + widgetsClient: WidgetsClient; + usersClient: UsersClient; + constructor(endpoint: string, options?: DemoServiceClientOptions) { + this.#context = createDemoServiceClientContext(endpoint, options); + this.widgetsClient = new WidgetsClient(endpoint, options); + this.usersClient = new UsersClient(endpoint, options); + } +} +``` + +It should have interface/function end-curly-braces with a newline before the next one, and no double-semicolons: + +```ts src/api/widgetsClient/widgetsClientContext.ts +import { type Client, type ClientOptions, getClient } from "@typespec/ts-http-runtime"; + +export interface WidgetsClientContext extends Client {} +export interface WidgetsClientOptions extends ClientOptions { + endpoint?: string; +} +export function createWidgetsClientContext( + endpoint: string, + options?: WidgetsClientOptions, +): WidgetsClientContext { + const params: Record = { + endpoint: endpoint + }; + const resolvedEndpoint = "{endpoint}".replace(/{([^}]+)}/g, (_, key) => + key in params ? String(params[key]) : (() => { throw new Error(`Missing parameter: ${key}`); })() + ); + return getClient(resolvedEndpoint,{ + ...options + }) +} + +```