From e8b26e291ab32ab68b92eb68b2a085d811e1c0e0 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Tue, 13 Jan 2026 11:51:03 -0800 Subject: [PATCH 1/8] Add declaration provider --- .../src/core/context/declaration-provider.ts | 21 ++ .../src/core/custom-emit.tss | 122 +++++++ .../src/core/declaration-provider.test.tsx | 308 ++++++++++++++++++ .../src/core/declaration-provider.ts | 132 ++++++++ .../components/enum-declaration.tsx | 26 +- .../components/function-declaration.tsx | 6 +- .../components/interface-declaration.tsx | 11 +- .../components/static-serializers.tsx | 19 +- .../components/type-alias-declaration.tsx | 6 +- .../components/type-declaration.tsx | 9 +- .../typescript/components/type-expression.tsx | 37 +-- .../typescript/components/type-transform.tsx | 15 +- .../components/union/declaration.tsx | 26 +- .../components/union/expression.tsx | 6 +- .../src/typescript/utils/operation.ts | 14 +- .../src/typescript/utils/refkey.ts | 37 +-- .../components/enum-declaration.test.tsx | 64 ++-- .../http-client-js/src/utils/parameters.tsx | 2 +- packages/http-client-js/test.tsp | 40 +++ 19 files changed, 743 insertions(+), 158 deletions(-) create mode 100644 packages/emitter-framework/src/core/context/declaration-provider.ts create mode 100644 packages/emitter-framework/src/core/custom-emit.tss create mode 100644 packages/emitter-framework/src/core/declaration-provider.test.tsx create mode 100644 packages/emitter-framework/src/core/declaration-provider.ts create mode 100644 packages/http-client-js/test.tsp diff --git a/packages/emitter-framework/src/core/context/declaration-provider.ts b/packages/emitter-framework/src/core/context/declaration-provider.ts new file mode 100644 index 00000000000..6aa554aa88b --- /dev/null +++ b/packages/emitter-framework/src/core/context/declaration-provider.ts @@ -0,0 +1,21 @@ +import { DeclarationProvider } from "#core/declaration-provider.js"; +import { type ComponentContext, createNamedContext, useContext } from "@alloy-js/core"; +import type { Typekit } from "@typespec/compiler/typekit"; +import { useTsp } from "./tsp-context.js"; + +export const DeclarationProviderContext: ComponentContext = + createNamedContext("DeclarationProviderContext"); + +export function useDeclarationProvider(): DeclarationProvider { + return useContext(DeclarationProviderContext) ?? getDefaultDeclarationProvider(useTsp().$); +} + +const knownDeclarationProviders = new WeakMap(); +function getDefaultDeclarationProvider($: Typekit) { + let provider = knownDeclarationProviders.get($); + if (!provider) { + provider = new DeclarationProvider($); + knownDeclarationProviders.set($, provider); + } + return provider; +} diff --git a/packages/emitter-framework/src/core/custom-emit.tss b/packages/emitter-framework/src/core/custom-emit.tss new file mode 100644 index 00000000000..b564af13121 --- /dev/null +++ b/packages/emitter-framework/src/core/custom-emit.tss @@ -0,0 +1,122 @@ +import { Refkey } from "@alloy-js/core"; +import { Children, ComponentDefinition } from "@alloy-js/core/jsx-runtime"; +import { Enum, EnumMember, ModelProperty, Program, Scalar, Type } from "@typespec/compiler"; +import { UnionVariant } from "@typespec/compiler/src/core/types"; +import { $ } from "@typespec/compiler/typekit"; + +const getEmitOptionsForTypeSym: unique symbol = Symbol.for( + "@typespec/emitter-framework:getEmitOptionsForType", +); + +const getEmitOptionsForTypeKindSym: unique symbol = Symbol.for( + "@typespec/emitter-framework:getEmitOptionsForTypeKind", +); + +export function typeEmitOptions() { + return new TypeEmitOptionsBuilder(); +} + +typeEmitOptions().forTypeKind("Enum", { + declare(props) { + props.type; + }, +}); + +declare const foo: Enum; +typeEmitOptions().forType(foo, { + declare(props) { + props.type; + }, +}); +export class TypeEmitOptionsBuilder { + #typeEmitOptions: Map> = new Map(); + #typeKindEmitOptions: Map> = new Map(); + + forType(type: T, options: TypeEmitOptions) { + this.#typeEmitOptions.set(type, options); + + return this; + } + + forTypeKind( + typeKind: TKind, + options: TypeEmitOptions>, + ) { + this.#typeKindEmitOptions.set(typeKind, options); + + return this; + } + + /** + * @internal + */ + [getEmitOptionsForTypeSym](program: Program, type: Type) { + let options = this.#typeEmitOptions.get(type); + if (options || !$(program).scalar.is(type) || program.checker.isStdType(type)) { + return options; + } + + // have a scalar, it's not a built-in scalar, and didn't find options, so + // see if we have options for a base scalar. + let currentScalar: Scalar | undefined = type; + while ( + currentScalar && + !program.checker.isStdType(type) && + !this.#typeEmitOptions.has(currentScalar) + ) { + currentScalar = currentScalar?.baseScalar; + } + + if (!currentScalar) { + return undefined; + } + + return this.#typeEmitOptions.get(currentScalar); + } + + /** + * @internal + */ + [getEmitOptionsForTypeKindSym](program: Program, typeKind: Type["kind"]) { + return this.#typeKindEmitOptions.get(typeKind); + } +} + +interface TypeEmitOptionsDeclarationDefaultProps { + name: string; + refkey: Refkey | Refkey[]; +} + +interface TypeEmitOptionsReferenceDefaultProps {} + +interface TypeEmitOptionsDeclarationProps { + type: TType; + default: Children; + defaultProps: TypeEmitOptionsDeclarationDefaultProps; +} + +interface TypeEmitOptionsExpressionProps { + type: TType; + default: Children; + defaultProps: TypeEmitOptionsReferenceDefaultProps; + member?: UnionVariant | EnumMember | ModelProperty; +} + +interface TypeEmitOptions< + TType extends Type, + TDeclarationProps = TypeEmitOptionsDeclarationProps, + TExpressionProps = TypeEmitOptionsExpressionProps, + TReferenceProps = TypeEmitOptionsReferenceProps, +> { + declaration?: ComponentDefinition; + expression?: ComponentDefinition; + reference?: ComponentDefinition; + noDeclaration?: boolean; + noReference?: boolean; + noExpression?: boolean; +} + +typeEmitOptions().forTypeKind("Enum", {}).forTypeKind("Scalar", {}).forTypeKind("Union", {}); + +interface ZodTypeEmitOptionsDeclarationProps + extends TypeEmitOptionsDeclarationProps {} diff --git a/packages/emitter-framework/src/core/declaration-provider.test.tsx b/packages/emitter-framework/src/core/declaration-provider.test.tsx new file mode 100644 index 00000000000..57377493988 --- /dev/null +++ b/packages/emitter-framework/src/core/declaration-provider.test.tsx @@ -0,0 +1,308 @@ +import { Tester } from "#test/test-host.js"; +import { effect, Output, renderAsync } from "@alloy-js/core"; +import type { Enum, Model, Union } from "@typespec/compiler"; +import { $ as typekit } from "@typespec/compiler/typekit"; +import { describe, expect, it } from "vitest"; +import { DeclarationProvider } from "./declaration-provider.js"; + +describe("isDeclaration", () => { + interface DeclarationTestCase { + description: string; + code: string; + expected: boolean; + } + + async function assertDeclaration(code: string, expected: boolean) { + const runner = await Tester.createInstance(); + const { program } = await runner.compile(code); + const $ = typekit(program); + const provider = new DeclarationProvider($); + + const type = program.resolveTypeReference("Test")[0]!; + expect(type, "Type Test should exist").toBeDefined(); + expect(provider.isDeclaration(type)).toBe(expected); + } + + async function assertDeclarationExpression(expression: string, expected: boolean, setup = "") { + const code = `${setup}model Test { prop: ${expression}; }`; + const runner = await Tester.createInstance(); + const { program } = await runner.compile(code); + const $ = typekit(program); + const provider = new DeclarationProvider($); + + const test = program.resolveTypeReference("Test")[0] as Model; + const type = test.properties.get("prop")!.type; + expect(type, "Property type should exist").toBeDefined(); + expect(provider.isDeclaration(type)).toBe(expected); + } + + const declarationTestCases: DeclarationTestCase[] = [ + { + description: "named model", + code: `model Test { name: string; }`, + expected: true, + }, + { + description: "namespace", + code: `namespace Test { }`, + expected: true, + }, + { + description: "interface", + code: `interface Test { getPet(): string; }`, + expected: true, + }, + { + description: "enum", + code: `enum Test { Red, Green, Blue }`, + expected: true, + }, + { + description: "operation", + code: `op Test(): string;`, + expected: true, + }, + { + description: "named union", + code: `union Test { string, int32 }`, + expected: true, + }, + { + description: "scalar", + code: `scalar Test extends string;`, + expected: true, + }, + ]; + + declarationTestCases.forEach(({ description, code, expected }) => { + it(`treats ${description} as declaration`, async () => { + await assertDeclaration(code, expected); + }); + }); + + interface ExpressionTestCase { + description: string; + expression: string; + expected: boolean; + setup?: string; + } + + const expressionTestCases: ExpressionTestCase[] = [ + { + description: "array", + expression: "string[]", + expected: false, + }, + { + description: "record", + expression: "Record", + expected: false, + }, + { + description: "anonymous union", + expression: '"active" | "inactive"', + expected: false, + }, + { + description: "anonymous model", + expression: "{ name: string }", + expected: false, + }, + { + description: "template instance", + expression: "Array", + expected: false, + }, + { + description: "scalar (string)", + expression: "string", + expected: false, + }, + { + description: "custom template instance", + expression: "Container", + expected: true, + setup: "model Container { item: T; }", + }, + { + description: "intrinsic (void)", + expression: "void", + expected: false, + }, + { + description: "intrinsic (never)", + expression: "never", + expected: false, + }, + ]; + + expressionTestCases.forEach(({ description, expression, expected, setup }) => { + it(`treats ${description} as ${expected ? "declaration" : "non-declaration"}`, async () => { + await assertDeclarationExpression(expression, expected, setup); + }); + }); + + it("treats a model property as a non-declaration", async () => { + const runner = await Tester.createInstance(); + const { program } = await runner.compile(`model Test { a: string, b: int32 }`); + const $ = typekit(program); + const provider = new DeclarationProvider($); + + const model = program.resolveTypeReference("Test")[0]! as Model; + const property = model.properties.get("a")!; + expect(provider.isDeclaration(property)).toBe(false); + }); + + it("treats union variant as non-declaration", async () => { + const runner = await Tester.createInstance(); + const { program } = await runner.compile(`union Test { a: string, b: int32 }`); + const $ = typekit(program); + const provider = new DeclarationProvider($); + + const union = program.resolveTypeReference("Test")[0]! as Union; + const variant = union.variants.get("a")!; + expect(provider.isDeclaration(variant)).toBe(false); + }); + + it("treats enum member as non-declaration", async () => { + const runner = await Tester.createInstance(); + const { program } = await runner.compile(`enum Test { Red, Green, Blue }`); + const $ = typekit(program); + const provider = new DeclarationProvider($); + + const enumType = program.resolveTypeReference("Test")[0]! as Enum; + const member = enumType.members.get("Red")!; + expect(provider.isDeclaration(member)).toBe(false); + }); +}); + +describe("getRefkey", () => { + it("creates a refkey for a declaration type", async () => { + const runner = await Tester.createInstance(); + const { program } = await runner.compile(` + model Pet { + name: string; + } + `); + const $ = typekit(program); + const provider = new DeclarationProvider($); + + const pet = program.resolveTypeReference("Pet")[0]!; + const key = provider.getRefkey(pet); + + expect(key).toBeDefined(); + }); + + it("returns the same refkey for the same type", async () => { + const runner = await Tester.createInstance(); + const { program } = await runner.compile(` + model Pet { + name: string; + } + `); + const $ = typekit(program); + const provider = new DeclarationProvider($); + + const pet = program.resolveTypeReference("Pet")[0]!; + const key1 = provider.getRefkey(pet); + const key2 = provider.getRefkey(pet); + + expect(key1).toBe(key2); + }); + + it("returns different refkeys for different types", async () => { + const runner = await Tester.createInstance(); + const { program } = await runner.compile(` + model Pet { + name: string; + } + model Owner { + name: string; + } + `); + const $ = typekit(program); + const provider = new DeclarationProvider($); + + const pet = program.resolveTypeReference("Pet")[0]!; + const owner = program.resolveTypeReference("Owner")[0]!; + const petKey = provider.getRefkey(pet); + const ownerKey = provider.getRefkey(owner); + + expect(petKey).not.toBe(ownerKey); + }); + + it("throws for non-declaration types", async () => { + const runner = await Tester.createInstance(); + const { program } = await runner.compile(` + model Pet { + name: string; + } + `); + const $ = typekit(program); + const provider = new DeclarationProvider($); + + const pet = program.resolveTypeReference("Pet")[0] as Model; + const prop = pet.properties.get("name")!; + + expect(() => provider.getRefkey(prop)).toThrow( + "Type ModelProperty is not a declaration type and cannot have a refkey.", + ); + }); + + it("adds type to declarations map", async () => { + const runner = await Tester.createInstance(); + const { program } = await runner.compile(` + model Pet { + name: string; + } + `); + const $ = typekit(program); + const provider = new DeclarationProvider($); + + const pet = program.resolveTypeReference("Pet")[0]!; + + // Initially not in the map + expect(provider.declarations.has(pet)).toBe(false); + + // Get refkey should add it + const key = provider.getRefkey(pet); + + // Now it should be in the map + expect(provider.declarations.has(pet)).toBe(true); + expect(provider.declarations.get(pet)).toBe(key); + }); + + it("declarations map is reactive", async () => { + const runner = await Tester.createInstance(); + const { program } = await runner.compile(` + model Pet { + name: string; + } + `); + const $ = typekit(program); + const provider = new DeclarationProvider($); + + const pet = program.resolveTypeReference("Pet")[0]!; + let effectRan = false; + let capturedKey: any = undefined; + + // Set up an effect that reacts to the declarations map + effect(() => { + if (provider.declarations.has(pet)) { + effectRan = true; + capturedKey = provider.declarations.get(pet); + } + }); + + // Initially effect should not have run (map is empty) + expect(effectRan).toBe(false); + + // Get refkey should trigger the effect + const key = provider.getRefkey(pet); + await renderAsync(); // Allow reactivity to flush + + // Effect should have run and captured the key + expect(effectRan).toBe(true); + expect(capturedKey).toBe(key); + }); +}); diff --git a/packages/emitter-framework/src/core/declaration-provider.ts b/packages/emitter-framework/src/core/declaration-provider.ts new file mode 100644 index 00000000000..0825b28ea84 --- /dev/null +++ b/packages/emitter-framework/src/core/declaration-provider.ts @@ -0,0 +1,132 @@ +import { refkey, shallowReactive, type Refkey } from "@alloy-js/core"; +import { getLocationContext, type MemberType, type Type } from "@typespec/compiler"; +import type { Typekit } from "@typespec/compiler/typekit"; + +export class DeclarationProvider { + $: Typekit; + declarations: Map = shallowReactive(new Map()); + #staticMemberRefkeys: Map = new Map(); + + constructor($: Typekit) { + this.$ = $; + } + + /** + * Get a refkey for the given type, creating one if one has not been created + * for it already. + * + * @throws if the type is not a declaration or static member. + * + * @remarks + * + * If you need a static member refkey for a type which is not otherwise a + * static member (e.g. because you are rendering a union which can't usually + * have its variants referenced as an enum which can), you can call + * `getStaticMemberRefkey` directly. + */ + public getRefkey(type: Type): Refkey { + if (this.isDeclaration(type)) { + return this.getDeclarationRefkey(type); + } + + if (this.isStaticMember(type)) { + return this.getStaticMemberRefkey(type as MemberType); + } + + throw new Error( + "Type ${type.kind} is not a declaration or static member and cannot have a refkey.", + ); + } + + /** + * Whether the given type should be referenced via refkey. True for things + * which are declarations and static members of declarations. + */ + public shouldReference(type: Type): boolean { + return this.isDeclaration(type) || this.isStaticMember(type); + } + + /** + * Get a refkey for the given declaration, creating one if one has not been + * created for it already. + * + * @throws if the type is not a declaration type. + */ + public getDeclarationRefkey(type: Type): Refkey { + const existing = this.declarations.get(type); + + if (existing) { + return existing; + } + + if (!this.isDeclaration(type)) { + throw new Error(`Type ${type.kind} is not a declaration type and cannot have a refkey.`); + } + + const key = refkey(); + this.declarations.set(type, key); + return key; + } + + /** + * Whether the given type should be emitted as a declaration. + * + * @remarks + * + * By default, things which are declarations in TypeSpec but not in the + * compiler are considered declarations. For example, `string` is not a + * declaration but `scalar customScalar extends string` is. Likewise, + * `Array` is not a declaration because Array is built-in, but + * `MyModel` is a declaration because it's not part of the compiler and + * has a TypeSpec declaration. + */ + public isDeclaration(type: Type): boolean { + const location = getLocationContext(this.$.program, type).type; + + if (location === "compiler") { + return false; + } + + if (!("name" in type) || type.name === undefined || type.name === "") { + return false; + } + + if ( + this.$.unionVariant.is(type) || + this.$.enumMember.is(type) || + this.$.modelProperty.is(type) || + this.$.intrinsic.is(type) + ) { + return false; + } + + if (location === "synthetic" && (type.name === "Record" || type.name === "Array")) { + return false; + } + + return true; + } + + /** + * Whether the given type is a static member that can be referenced directly + * (i.e. without instantiation). + */ + public isStaticMember(type: Type): boolean { + return this.$.enumMember.is(type); + } + + /** + * Get a refkey for the given static member, creating one if one has not been + * created for it already. + */ + public getStaticMemberRefkey(type: MemberType): Refkey { + const existing = this.#staticMemberRefkeys.get(type); + if (existing) { + return existing; + } + + const key = refkey(); + this.#staticMemberRefkeys.set(type, key); + return key; + } +} diff --git a/packages/emitter-framework/src/typescript/components/enum-declaration.tsx b/packages/emitter-framework/src/typescript/components/enum-declaration.tsx index 814824ebfab..dcffb06cb2e 100644 --- a/packages/emitter-framework/src/typescript/components/enum-declaration.tsx +++ b/packages/emitter-framework/src/typescript/components/enum-declaration.tsx @@ -1,9 +1,10 @@ +import { useDeclarationProvider } from "#core/context/declaration-provider.js"; +import { joinRefkeys } from "#typescript/utils/refkey.js"; import { type Children, For, type Refkey } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import type { Enum, EnumMember as TspEnumMember, Union } from "@typespec/compiler"; import { useTsp } from "../../core/context/tsp-context.js"; import { reportDiagnostic } from "../../lib.js"; -import { declarationRefkeys, efRefkey } from "../utils/refkey.js"; export interface EnumDeclarationProps extends Omit { name?: string; @@ -25,29 +26,32 @@ export function EnumDeclaration(props: EnumDeclarationProps) { if (!props.type.name || props.type.name === "") { reportDiagnostic($.program, { code: "type-declaration-missing-name", target: props.type }); } - const refkeys = declarationRefkeys(props.refkey, props.type); - const name = props.name ?? ts.useTSNamePolicy().getName(props.type.name!, "enum"); - const members = Array.from(type.members.entries()); + const dp = useDeclarationProvider(); + const refkeys = joinRefkeys(props.refkey, dp.getRefkey(props.type)); + const members = type.members.values(); const doc = props.doc ?? $.type.getDoc(type); return ( - {([key, value]) => { - const memberDoc = $.type.getDoc(value); + {(member) => { + const memberDoc = $.type.getDoc(member); + + const originalMember = $.union.is(props.type) + ? props.type.variants.get(member.name)! + : member; + return ( ); }} diff --git a/packages/emitter-framework/src/typescript/components/function-declaration.tsx b/packages/emitter-framework/src/typescript/components/function-declaration.tsx index 41917a65e15..13740629ba0 100644 --- a/packages/emitter-framework/src/typescript/components/function-declaration.tsx +++ b/packages/emitter-framework/src/typescript/components/function-declaration.tsx @@ -1,8 +1,9 @@ +import { useDeclarationProvider } from "#core/context/declaration-provider.js"; +import { joinRefkeys } from "#typescript/utils/refkey.js"; import * as ts from "@alloy-js/typescript"; import type { Model, Operation } from "@typespec/compiler"; import { useTsp } from "../../core/index.js"; import { buildParameterDescriptors, getReturnType } from "../utils/operation.js"; -import { declarationRefkeys } from "../utils/refkey.js"; import { TypeExpression } from "./type-expression.js"; export interface FunctionDeclarationPropsWithType extends Omit< @@ -30,7 +31,8 @@ export function FunctionDeclaration(props: FunctionDeclarationProps) { return ; } - const refkeys = declarationRefkeys(props.refkey, props.type); + const dp = useDeclarationProvider(); + const refkeys = joinRefkeys(props.refkey, dp.getRefkey(props.type)); let name = props.name ? props.name : ts.useTSNamePolicy().getName(props.type.name, "function"); diff --git a/packages/emitter-framework/src/typescript/components/interface-declaration.tsx b/packages/emitter-framework/src/typescript/components/interface-declaration.tsx index cb1cc107919..75f1a8d36b8 100644 --- a/packages/emitter-framework/src/typescript/components/interface-declaration.tsx +++ b/packages/emitter-framework/src/typescript/components/interface-declaration.tsx @@ -1,3 +1,5 @@ +import { useDeclarationProvider } from "#core/context/declaration-provider.js"; +import { joinRefkeys } from "#typescript/utils/refkey.js"; import { type Children, For, mapJoin } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import { @@ -12,7 +14,6 @@ import type { Typekit } from "@typespec/compiler/typekit"; import { createRekeyableMap } from "@typespec/compiler/utils"; import { useTsp } from "../../core/context/tsp-context.js"; import { reportDiagnostic } from "../../lib.js"; -import { declarationRefkeys, efRefkey } from "../utils/refkey.js"; import { InterfaceMember } from "./interface-member.js"; import { TypeExpression } from "./type-expression.jsx"; export interface TypedInterfaceDeclarationProps extends Omit { @@ -40,8 +41,8 @@ export function InterfaceDeclaration(props: InterfaceDeclarationProps) { } name = namePolicy.getName(name, "interface"); - - const refkeys = declarationRefkeys(props.refkey, props.type); + const dp = useDeclarationProvider(); + const refkeys = joinRefkeys(props.refkey, dp.getRefkey(props.type)); const extendsType = props.extends ?? getExtendsType($, props.type); const doc = props.doc ?? $.type.getDoc(props.type); @@ -83,7 +84,7 @@ function getExtendsType($: Typekit, type: Model | Interface): Children | undefin if (!$.model.is(type)) { return undefined; } - + const dp = useDeclarationProvider(); const extending: Children[] = []; if (type.baseModel) { @@ -94,7 +95,7 @@ function getExtendsType($: Typekit, type: Model | Interface): Children | undefin // Instead of extending we need to create an envelope property // do nothing here. } else { - extending.push(efRefkey(type.baseModel)); + extending.push(dp.getRefkey(type.baseModel)); } } diff --git a/packages/emitter-framework/src/typescript/components/static-serializers.tsx b/packages/emitter-framework/src/typescript/components/static-serializers.tsx index 991c09e5b9f..f03ab38b677 100644 --- a/packages/emitter-framework/src/typescript/components/static-serializers.tsx +++ b/packages/emitter-framework/src/typescript/components/static-serializers.tsx @@ -1,8 +1,7 @@ -import { code } from "@alloy-js/core"; +import { code, refkey } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; -import { efRefkey } from "../utils/refkey.js"; -export const DateRfc3339SerializerRefkey = efRefkey(); +export const DateRfc3339SerializerRefkey = refkey(); export function DateRfc3339Serializer() { return ( `; const convertFnType = `(item: any) => any`; @@ -161,7 +160,7 @@ export function RecordSerializer() { ); } -export const ArraySerializerRefkey = efRefkey(); +export const ArraySerializerRefkey = refkey(); export function ArraySerializer() { const arrayType = `any[]`; const convertFnType = `(item: any) => any`; diff --git a/packages/emitter-framework/src/typescript/components/type-alias-declaration.tsx b/packages/emitter-framework/src/typescript/components/type-alias-declaration.tsx index 17bac4e0c83..f71ffa7c59d 100644 --- a/packages/emitter-framework/src/typescript/components/type-alias-declaration.tsx +++ b/packages/emitter-framework/src/typescript/components/type-alias-declaration.tsx @@ -1,8 +1,9 @@ +import { useDeclarationProvider } from "#core/context/declaration-provider.js"; +import { joinRefkeys } from "#typescript/utils/refkey.js"; import * as ts from "@alloy-js/typescript"; import type { Type } from "@typespec/compiler"; import { useTsp } from "../../core/context/tsp-context.js"; import { reportDiagnostic } from "../../lib.js"; -import { declarationRefkeys } from "../utils/refkey.js"; import { TypeExpression } from "./type-expression.jsx"; export interface TypedAliasDeclarationProps extends Omit { @@ -31,7 +32,8 @@ export function TypeAliasDeclaration(props: TypeAliasDeclarationProps) { } const doc = props.doc ?? $.type.getDoc(props.type); - const refkeys = declarationRefkeys(props.refkey, props.type); + const dp = useDeclarationProvider(); + const refkeys = joinRefkeys(props.refkey, dp.getRefkey(props.type)); const name = ts.useTSNamePolicy().getName(originalName, "type"); return ( diff --git a/packages/emitter-framework/src/typescript/components/type-declaration.tsx b/packages/emitter-framework/src/typescript/components/type-declaration.tsx index ac62e5b76af..9f4224afef3 100644 --- a/packages/emitter-framework/src/typescript/components/type-declaration.tsx +++ b/packages/emitter-framework/src/typescript/components/type-declaration.tsx @@ -1,7 +1,6 @@ import * as ts from "@alloy-js/typescript"; import type { Type } from "@typespec/compiler"; import { useTsp } from "../../core/index.js"; -import { declarationRefkeys } from "../utils/refkey.js"; import { EnumDeclaration } from "./enum-declaration.js"; import { InterfaceDeclaration } from "./interface-declaration.jsx"; import { TypeAliasDeclaration } from "./type-alias-declaration.jsx"; @@ -17,13 +16,7 @@ export type WithRequired = T & { [P in K]-?: T[P] }; export function TypeDeclaration(props: TypeDeclarationProps) { const { $ } = useTsp(); if (!props.type) { - const refkeys = declarationRefkeys(props.refkey, props.type); - return ( - )} - refkey={refkeys} - /> - ); + return )} />; } const { type, ...restProps } = props; diff --git a/packages/emitter-framework/src/typescript/components/type-expression.tsx b/packages/emitter-framework/src/typescript/components/type-expression.tsx index 18123f64258..3308015c4be 100644 --- a/packages/emitter-framework/src/typescript/components/type-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/type-expression.tsx @@ -1,11 +1,11 @@ +import { useDeclarationProvider } from "#core/context/declaration-provider.js"; import { For } from "@alloy-js/core"; -import { Reference, ValueExpression } from "@alloy-js/typescript"; +import { 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"; import { ArrayExpression } from "./array-expression.js"; import { FunctionType } from "./function-type.js"; import { InterfaceExpression } from "./interface-declaration.js"; @@ -30,11 +30,10 @@ export function TypeExpression(props: TypeExpressionProps) { return ( {() => { - if (!props.noReference && isDeclaration($, type)) { - // todo: probably need abstraction around deciding what's a declaration in the output - // (it may not correspond to things which are declarations in TypeSpec?) - return ; - //throw new Error("Reference not implemented"); + const dp = useDeclarationProvider(); + if (!props.noReference && dp.shouldReference(type)) { + console.log("Referencing " + type.kind); + return dp.getRefkey(type); } // TODO: Make sure this is an exhaustive switch, including EnumMember and such @@ -159,27 +158,3 @@ function getScalarIntrinsicExpression($: Typekit, type: Scalar | IntrinsicType): return tsType; } - -function isDeclaration($: Typekit, type: Type): boolean { - switch (type.kind) { - case "Namespace": - case "Interface": - case "Enum": - case "Operation": - case "EnumMember": - return true; - case "UnionVariant": - return false; - - case "Model": - if ($.array.is(type) || $.record.is(type)) { - return false; - } - - return Boolean(type.name); - case "Union": - return Boolean(type.name); - default: - return false; - } -} diff --git a/packages/emitter-framework/src/typescript/components/type-transform.tsx b/packages/emitter-framework/src/typescript/components/type-transform.tsx index 0e629db42e6..539976bd9df 100644 --- a/packages/emitter-framework/src/typescript/components/type-transform.tsx +++ b/packages/emitter-framework/src/typescript/components/type-transform.tsx @@ -1,4 +1,4 @@ -import { type Children, code, For, mapJoin, type Refkey } from "@alloy-js/core"; +import { type Children, code, For, mapJoin, refkey, type Refkey } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import type { Discriminator, @@ -14,7 +14,8 @@ import { createRekeyableMap } from "@typespec/compiler/utils"; import { useTsp } from "../../core/context/tsp-context.js"; import { reportDiagnostic } from "../../lib.js"; import { reportTypescriptDiagnostic } from "../../typescript/lib.js"; -import { efRefkey } from "../utils/refkey.js"; + +import { useDeclarationProvider } from "#core/context/declaration-provider.js"; import { ArraySerializerRefkey, DateDeserializerRefkey, @@ -122,11 +123,12 @@ export function TypeTransformDeclaration(props: TypeTransformProps) { }); } + const dp = useDeclarationProvider(); const baseName = namePolicy.getName(originalName!, "function"); const functionSuffix = props.target === "application" ? "ToApplication" : "ToTransport"; const functionName = props.name ? props.name : `${baseName}${functionSuffix}`; const itemType = - props.target === "application" ? "any" : ; + props.target === "application" ? "any" : ; let transformExpression: Children; if ($.model.is(props.type)) { @@ -153,7 +155,7 @@ export function TypeTransformDeclaration(props: TypeTransformProps) { }); } - const returnType = props.target === "application" ? efRefkey(props.type) : "any"; + const returnType = props.target === "application" ? dp.getRefkey(props.type) : "any"; const ref = props.refkey ?? getTypeTransformerRefkey(props.type, props.target); @@ -177,7 +179,7 @@ export function TypeTransformDeclaration(props: TypeTransformProps) { * @returns the refkey for the TypeTransformer function */ export function getTypeTransformerRefkey(type: Type, target: "application" | "transport") { - return efRefkey(type, target); + return refkey(type, target); } export interface ModelTransformExpressionProps { @@ -363,8 +365,9 @@ export function TypeTransformCall(props: TypeTransformCallProps): Children { itemPath.unshift(`${props.optionsBagName}?`); } let itemName: Children = itemPath.join("."); + const dp = useDeclarationProvider(); if (props.castInput) { - itemName = code`${itemName} as ${efRefkey(props.type)}`; + itemName = code`${itemName} as ${dp.getRefkey(props.type)}`; } const transformType = collapsedProperty?.type ?? props.type; if ($.model.is(transformType) && $.array.is(transformType)) { diff --git a/packages/emitter-framework/src/typescript/components/union/declaration.tsx b/packages/emitter-framework/src/typescript/components/union/declaration.tsx index 7c444c5565a..22def245b69 100644 --- a/packages/emitter-framework/src/typescript/components/union/declaration.tsx +++ b/packages/emitter-framework/src/typescript/components/union/declaration.tsx @@ -1,5 +1,6 @@ -import { declarationRefkeys } from "#typescript/utils/refkey.js"; -import { type Children } from "@alloy-js/core"; +import { useDeclarationProvider } from "#core/context/declaration-provider.js"; +import { joinRefkeys } from "#typescript/utils/refkey.js"; +import { splitProps, type Children } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import type { Enum, Union } from "@typespec/compiler"; import { useTsp } from "../../../core/context/tsp-context.js"; @@ -14,26 +15,29 @@ export interface TypedUnionDeclarationProps extends Omit{props.children}; + return ; } - const { type, ...coreProps } = props; - const refkeys = declarationRefkeys(props.refkey, props.type); - - const originalName = coreProps.name ?? type.name; + const [typeProp, coreProps] = splitProps(props, ["type"]); + const type = typeProp.type; + const originalName = coreProps.name ?? type.name ?? ""; - if (!originalName || originalName === "") { + if (originalName === "") { reportDiagnostic($.program, { code: "type-declaration-missing-name", target: type }); } - const name = ts.useTSNamePolicy().getName(originalName!, "type"); - + const dp = useDeclarationProvider(); + const refkey = joinRefkeys(props.refkey, dp.getRefkey(type)); const doc = props.doc ?? $.type.getDoc(type); return ( - + {coreProps.children} ); diff --git a/packages/emitter-framework/src/typescript/components/union/expression.tsx b/packages/emitter-framework/src/typescript/components/union/expression.tsx index 2d8df922dd5..e250b3af5d8 100644 --- a/packages/emitter-framework/src/typescript/components/union/expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union/expression.tsx @@ -1,3 +1,4 @@ +import { useDeclarationProvider } from "#core/context/declaration-provider.js"; import { type Children, For, List } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import { @@ -8,7 +9,6 @@ import { type UnionVariant, } from "@typespec/compiler"; import { useTsp } from "../../../core/context/tsp-context.js"; -import { efRefkey } from "../../utils/refkey.js"; import { TypeExpression } from "../type-expression.jsx"; export interface UnionExpressionProps { @@ -125,6 +125,8 @@ function NoneEnvelope(props: NoneEnvelopeProps) { return ; } + const dp = useDeclarationProvider(); + return ( @@ -133,7 +135,7 @@ function NoneEnvelope(props: NoneEnvelopeProps) { value={} /> - <>{efRefkey(props.type.type)} + <>{dp.getRefkey(props.type.type)} ); } diff --git a/packages/emitter-framework/src/typescript/utils/operation.ts b/packages/emitter-framework/src/typescript/utils/operation.ts index aa73b346ad2..ae7a967f7f9 100644 --- a/packages/emitter-framework/src/typescript/utils/operation.ts +++ b/packages/emitter-framework/src/typescript/utils/operation.ts @@ -1,9 +1,8 @@ -import { refkey, type Refkey } from "@alloy-js/core"; +import { type Refkey } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import type { Model, ModelProperty, Operation, Type } from "@typespec/compiler"; import { useTsp } from "../../core/index.js"; import { TypeExpression } from "../components/type-expression.jsx"; -import { efRefkey } from "./refkey.js"; export function getReturnType( type: Operation, @@ -30,7 +29,6 @@ export function buildParameterDescriptors( options: BuildParameterDescriptorsOptions = {}, ): ts.ParameterDescriptor[] | undefined { const { $ } = useTsp(); - const suffixRefkey = options.suffixRefkey ?? refkey(); const optionsParams = normalizeParameters(options.params); if (options.mode === "replace") { @@ -38,9 +36,7 @@ export function buildParameterDescriptors( } const modelProperties = $.model.getProperties(type); - const operationParams = [...modelProperties.values()].map((m) => - buildParameterDescriptor(m, suffixRefkey), - ); + const operationParams = [...modelProperties.values()].map((m) => buildParameterDescriptor(m)); // Merge parameters based on location const allParams = @@ -51,10 +47,7 @@ export function buildParameterDescriptors( return allParams; } -export function buildParameterDescriptor( - modelProperty: ModelProperty, - suffixRefkey: Refkey, -): ts.ParameterDescriptor { +export function buildParameterDescriptor(modelProperty: ModelProperty): ts.ParameterDescriptor { const { $ } = useTsp(); const namePolicy = ts.useTSNamePolicy(); const paramName = namePolicy.getName(modelProperty.name, "parameter"); @@ -63,7 +56,6 @@ export function buildParameterDescriptor( return { doc, name: paramName, - refkey: efRefkey(modelProperty, suffixRefkey), optional: isOptional, type: TypeExpression({ type: modelProperty.type }), }; diff --git a/packages/emitter-framework/src/typescript/utils/refkey.ts b/packages/emitter-framework/src/typescript/utils/refkey.ts index 6cf84fb5154..bb6149d02bb 100644 --- a/packages/emitter-framework/src/typescript/utils/refkey.ts +++ b/packages/emitter-framework/src/typescript/utils/refkey.ts @@ -1,36 +1,5 @@ -import { refkey as ayRefkey, type Refkey } from "@alloy-js/core"; +import type { Refkey } from "@alloy-js/core"; -const refKeyPrefix = Symbol.for("emitter-framework:typescript"); - -/** - * A wrapper around `refkey` that uses a custom symbol to avoid collisions with - * other libraries that use `refkey`. - * - * @remarks - * - * The underlying refkey function is called with the {@link refKeyPrefix} symbol as the first argument. - * - * @param args The parameters of the refkey. - * @returns A refkey object that can be used to identify the value. - */ -export function efRefkey(...args: unknown[]): Refkey { - if (args.length === 0) { - return ayRefkey(); // Generates a unique refkey - } - return ayRefkey(refKeyPrefix, ...args); -} - -/** - * Creates a refkey for a declaration by combining the provided refkey with an internal - * refkey generated from the provided arguments. - * - * @param refkey The refkey provided by the user to be passed as is. - * @param args The parameters of the refkey. - * @returns An array of refkeys that can be passed to an Alloy declaration. - */ -export function declarationRefkeys(refkey?: Refkey | Refkey[], ...args: unknown[]): Refkey[] { - if (refkey) { - return [refkey, efRefkey(...args)].flat(); - } - return [efRefkey(...args)]; +export function joinRefkeys(...refkeys: (Refkey | Refkey[] | undefined)[]): Refkey[] { + return refkeys.filter((v) => !!v).flat(); } diff --git a/packages/emitter-framework/test/typescript/components/enum-declaration.test.tsx b/packages/emitter-framework/test/typescript/components/enum-declaration.test.tsx index 5e224c45856..d1d4921bd41 100644 --- a/packages/emitter-framework/test/typescript/components/enum-declaration.test.tsx +++ b/packages/emitter-framework/test/typescript/components/enum-declaration.test.tsx @@ -1,10 +1,12 @@ +import { DeclarationProviderContext } from "#core/context/declaration-provider.js"; +import { DeclarationProvider } from "#core/declaration-provider.js"; import { List, StatementList } from "@alloy-js/core"; import { d } from "@alloy-js/core/testing"; -import type { Enum, Union } from "@typespec/compiler"; +import type { Enum, Program, Union } from "@typespec/compiler"; +import { $ } from "@typespec/compiler/typekit"; import { describe, expect, it } from "vitest"; import { TspContext } from "../../../src/core/index.js"; import { EnumDeclaration } from "../../../src/typescript/components/enum-declaration.js"; -import { efRefkey } from "../../../src/typescript/utils/refkey.js"; import { getEmitOutput } from "../../utils.js"; describe("Typescript Enum Declaration", () => { @@ -141,19 +143,26 @@ describe("Typescript Enum Declaration", () => { } `; - const output = await getEmitOutput(code, (program) => { - const Foo = program.resolveTypeReference("Foo")[0]! as Enum; + function Test(props: { program: Program }) { + const dp = new DeclarationProvider($(props.program)); + const Foo = props.program.resolveTypeReference("Foo")[0]! as Enum; return ( - - - - - {efRefkey(Foo)} - {efRefkey(Foo.members.get("one"))} - - - + + + + + + {dp.getRefkey(Foo)} + {dp.getStaticMemberRefkey(Foo.members.get("one")!)} + + + + ); + } + + const output = await getEmitOutput(code, (program) => { + return ; }); expect(output).toBe(d` @@ -176,19 +185,26 @@ describe("Typescript Enum Declaration", () => { } `; - const output = await getEmitOutput(code, (program) => { - const Foo = program.resolveTypeReference("Foo")[0]! as Union; + function Test(props: { program: Program }) { + const dp = new DeclarationProvider($(props.program)); + const Foo = props.program.resolveTypeReference("Foo")[0]! as Union; return ( - - - - - {efRefkey(Foo)} - {efRefkey(Foo.variants.get("one"))} - - - + + + + + + {dp.getRefkey(Foo)} + {dp.getStaticMemberRefkey(Foo.variants.get("one")!)} + + + + ); + } + + const output = await getEmitOutput(code, (program) => { + return ; }); expect(output).toBe(d` diff --git a/packages/http-client-js/src/utils/parameters.tsx b/packages/http-client-js/src/utils/parameters.tsx index af058b37e73..076e4ccdeea 100644 --- a/packages/http-client-js/src/utils/parameters.tsx +++ b/packages/http-client-js/src/utils/parameters.tsx @@ -70,7 +70,7 @@ function buildClientParameterDescriptor( }; } - return buildParameterDescriptor(modelProperty, suffixRefkey); + return buildParameterDescriptor(modelProperty); } const oauth2FlowRefs: Record = { diff --git a/packages/http-client-js/test.tsp b/packages/http-client-js/test.tsp new file mode 100644 index 00000000000..82fe8a0246f --- /dev/null +++ b/packages/http-client-js/test.tsp @@ -0,0 +1,40 @@ +import "@typespec/http"; + +using Http; +@service(#{ title: "Widget Service" }) +namespace DemoService; + +model Widget { + @visibility(Lifecycle.Read) + id: string; + + weight: int32; + color: "red" | "blue"; +} + +@error +model Error { + code: int32; + message: string; +} + +@route("/widgets") +@tag("Widgets") +interface Widgets { + /** List widgets */ + @get list(): Widget[] | Error; + + /** Read widgets */ + @get read(@path id: Widget.id): Widget | Error; + + /** Create a widget */ + @post create(@body widget: Widget): Widget | Error; + + /** Update a widget */ + @patch update(@path id: Widget.id, @body widget: MergePatchUpdate): Widget | Error; + + /** Delete a widget */ + @delete delete(@path id: Widget.id): void | Error; + /** Analyze a widget */ + @route("{id}/analyze") @post analyze(@path id: Widget.id): string | Error; +} From 4048e1bc8e1a1b60322e3bec425dff83604baccf Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Wed, 14 Jan 2026 16:44:29 -0800 Subject: [PATCH 2/8] Progress toward fixing build --- packages/emitter-framework/package.json | 5 + .../src/core/declaration-provider.test.tsx | 4 +- .../src/core/declaration-provider.ts | 2 +- .../http-client-js/.dependency-cruiser.cjs | 382 ++++++++++++++++++ .../http-client-js/src/components/output.tsx | 2 +- .../static-helpers/bytes-encoding.tsx | 3 +- .../transforms/json/json-transform.tsx | 3 +- .../multipart/array-part-transform.tsx | 3 +- 8 files changed, 393 insertions(+), 11 deletions(-) create mode 100644 packages/http-client-js/.dependency-cruiser.cjs diff --git a/packages/emitter-framework/package.json b/packages/emitter-framework/package.json index 03e16d69534..c4eb7e03503 100644 --- a/packages/emitter-framework/package.json +++ b/packages/emitter-framework/package.json @@ -23,18 +23,23 @@ }, "exports": { ".": { + "development": "./src/core/index.ts", "import": "./dist/src/core/index.js" }, "./csharp": { + "development": "./src/csharp/index.ts", "import": "./dist/src/csharp/index.js" }, "./typescript": { + "development": "./src/typescript/index.ts", "import": "./dist/src/typescript/index.js" }, "./python": { + "development": "./src/python/index.ts", "import": "./dist/src/python/index.js" }, "./testing": { + "development": "./src/testing/index.ts", "import": "./dist/src/testing/index.js" } }, diff --git a/packages/emitter-framework/src/core/declaration-provider.test.tsx b/packages/emitter-framework/src/core/declaration-provider.test.tsx index 57377493988..4e45c52b6b0 100644 --- a/packages/emitter-framework/src/core/declaration-provider.test.tsx +++ b/packages/emitter-framework/src/core/declaration-provider.test.tsx @@ -244,9 +244,7 @@ describe("getRefkey", () => { const pet = program.resolveTypeReference("Pet")[0] as Model; const prop = pet.properties.get("name")!; - expect(() => provider.getRefkey(prop)).toThrow( - "Type ModelProperty is not a declaration type and cannot have a refkey.", - ); + expect(() => provider.getRefkey(prop)).toThrow(/Type ModelProperty is not a declaration/); }); it("adds type to declarations map", async () => { diff --git a/packages/emitter-framework/src/core/declaration-provider.ts b/packages/emitter-framework/src/core/declaration-provider.ts index 0825b28ea84..bf2f69fe33b 100644 --- a/packages/emitter-framework/src/core/declaration-provider.ts +++ b/packages/emitter-framework/src/core/declaration-provider.ts @@ -34,7 +34,7 @@ export class DeclarationProvider { } throw new Error( - "Type ${type.kind} is not a declaration or static member and cannot have a refkey.", + `Type ${type.kind} is not a declaration or static member and cannot have a refkey.`, ); } diff --git a/packages/http-client-js/.dependency-cruiser.cjs b/packages/http-client-js/.dependency-cruiser.cjs new file mode 100644 index 00000000000..190a31dae9e --- /dev/null +++ b/packages/http-client-js/.dependency-cruiser.cjs @@ -0,0 +1,382 @@ +/** @type {import('dependency-cruiser').IConfiguration} */ +module.exports = { + forbidden: [ + { + name: 'no-circular', + severity: 'warn', + comment: + "This dependency is part of a circular relationship. You might want to revise " + + "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ", + from: {}, + to: { + circular: true + } + }, + { + name: 'no-orphans', + comment: + "This is an orphan module - it's likely not used (anymore?). Either use it or " + + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + + "add an exception for it in your dependency-cruiser configuration. By default " + + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", + severity: 'warn', + from: { + orphan: true, + pathNot: [ + '(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files + '[.]d[.]ts$', // TypeScript declaration files + '(^|/)tsconfig[.]json$', // TypeScript config + '(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs + ] + }, + to: {}, + }, + { + name: 'no-deprecated-core', + comment: + 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + + "bound to exist - node doesn't deprecate lightly.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'core' + ], + path: [ + '^v8/tools/codemap$', + '^v8/tools/consarray$', + '^v8/tools/csvparser$', + '^v8/tools/logreader$', + '^v8/tools/profile_view$', + '^v8/tools/profile$', + '^v8/tools/SourceMap$', + '^v8/tools/splaytree$', + '^v8/tools/tickprocessor-driver$', + '^v8/tools/tickprocessor$', + '^node-inspect/lib/_inspect$', + '^node-inspect/lib/internal/inspect_client$', + '^node-inspect/lib/internal/inspect_repl$', + '^async_hooks$', + '^punycode$', + '^domain$', + '^constants$', + '^sys$', + '^_linklist$', + '^_stream_wrap$' + ], + } + }, + { + name: 'not-to-deprecated', + comment: + 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + + 'version of that module, or find an alternative. Deprecated modules are a security risk.', + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'deprecated' + ] + } + }, + { + name: 'no-non-package-json', + severity: 'error', + comment: + "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + + "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + + "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + + "in your package.json.", + from: {}, + to: { + dependencyTypes: [ + 'npm-no-pkg', + 'npm-unknown' + ] + } + }, + { + name: 'not-to-unresolvable', + comment: + "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + + 'module: add it to your package.json. In all other cases you likely already know what to do.', + severity: 'error', + from: {}, + to: { + couldNotResolve: true + } + }, + { + name: 'no-duplicate-dep-types', + comment: + "Likely this module depends on an external ('npm') package that occurs more than once " + + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + + "maintenance problems later on.", + severity: 'warn', + from: {}, + to: { + moreThanOneDependencyType: true, + // as it's common to use a devDependency for type-only imports: don't + // consider type-only dependencyTypes for this rule + dependencyTypesNot: ["type-only"] + } + }, + + // rules you might want to tweak for your specific situation: + { + name: 'not-to-test', + comment: + "This module depends on code within a folder that should only contain tests. As tests don't " + + "implement functionality this is odd. Either you're writing a test outside the test folder " + + "or there's something in the test folder that isn't a test.", + severity: 'error', + from: { + pathNot: '^(test)' + }, + to: { + path: '^(test)' + } + }, + { + name: 'not-to-spec', + comment: + 'This module depends on a spec (test) file. The responsibility of a spec file is to test code. ' + + "If there's something in a spec that's of use to other modules, it doesn't have that single " + + 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', + severity: 'error', + from: {}, + to: { + path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + } + }, + { + name: 'not-to-dev-dep', + severity: 'error', + comment: + "This module depends on an npm package from the 'devDependencies' section of your " + + 'package.json. It looks like something that ships to production, though. To prevent problems ' + + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + + 'section of your package.json. If this module is development only - add it to the ' + + 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', + from: { + path: '^(src)', + pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' + }, + to: { + dependencyTypes: [ + 'npm-dev', + ], + // type only dependencies are not a problem as they don't end up in the + // production code or are ignored by the runtime. + dependencyTypesNot: [ + 'type-only' + ], + pathNot: [ + 'node_modules/@types/' + ] + } + }, + { + name: 'optional-deps-used', + severity: 'info', + comment: + "This module depends on an npm package that is declared as an optional dependency " + + "in your package.json. As this makes sense in limited situations only, it's flagged here. " + + "If you use an optional dependency here by design - add an exception to your" + + "dependency-cruiser configuration.", + from: {}, + to: { + dependencyTypes: [ + 'npm-optional' + ] + } + }, + { + name: 'peer-deps-used', + comment: + "This module depends on an npm package that is declared as a peer dependency " + + "in your package.json. This makes sense if your package is e.g. a plugin, but in " + + "other cases - maybe not so much. If the use of a peer dependency is intentional " + + "add an exception to your dependency-cruiser configuration.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'npm-peer' + ] + } + } + ], + options: { + // Which modules not to follow further when encountered + doNotFollow: { + // path: an array of regular expressions in strings to match against + path: ['node_modules'] + }, + + // Which modules to exclude + // exclude : { + // // path: an array of regular expressions in strings to match against + // path: '', + // }, + + // Which modules to exclusively include (array of regular expressions in strings) + // dependency-cruiser will skip everything that doesn't match this pattern + // includeOnly : [''], + + // List of module systems to cruise. + // When left out dependency-cruiser will fall back to the list of _all_ + // module systems it knows of ('amd', 'cjs', 'es6', 'tsd']). It's the + // default because it's the safe option. It comes at a performance penalty, though + // As in practice only commonjs ('cjs') and ecmascript modules ('es6') + // are in wide use, you can limit the moduleSystems to those. + // moduleSystems: ['cjs', 'es6'], + + // false: don't look at JSDoc imports (the default) + // true: detect dependencies in JSDoc-style import statements. + // Implies parser: 'tsc', which a.o. means the typescript compiler will need + // to be installed in the same spot you run dependency-cruiser from. + // detectJSDocImports: true, + + // false: don't look at process.getBuiltinModule calls (the default) + // true: dependency-cruiser will detect calls to process.getBuiltinModule/ + // globalThis.process.getBuiltinModule as imports. + detectProcessBuiltinModuleCalls: true, + + // prefix for links in html, d2, mermaid and dot/ svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' + // to open it on your online repo or `vscode://file/${process.cwd()}/` to + // open it in visual studio code), + // prefix: `vscode://file/${process.cwd()}/`, + + // suffix for links in output. E.g. put .html here if you use it to link to + // your coverage reports. + // suffix: '.html', + + // false (the default): ignore dependencies that only exist before typescript-to-javascript compilation + // true: also detect dependencies that only exist before typescript-to-javascript compilation + // 'specify': for each dependency identify whether it only exists before compilation or also after + tsPreCompilationDeps: true, + + // list of extensions to scan that aren't javascript or compile-to-javascript. + // Empty by default. Only put extensions in here that you want to take into + // account that are _not_ parsable. + // extraExtensionsToScan: ['.json', '.jpg', '.png', '.svg', '.webp'], + + // if true combines the package.jsons found from the module up to the base + // folder the cruise is initiated from. Useful for how (some) mono-repos + // manage dependencies & dependency definitions. + // combinedDependencies: false, + + // if true leave symlinks untouched, otherwise use the realpath + // preserveSymlinks: false, + + // TypeScript project file ('tsconfig.json') to use for + // (1) compilation and + // (2) resolution (e.g. with the paths property) + // + // The (optional) fileName attribute specifies which file to take (relative to + // dependency-cruiser's current working directory). When not provided + // defaults to './tsconfig.json'. + tsConfig: { + fileName: 'tsconfig.json' + }, + + // Webpack configuration to use to get resolve options from. + // + // The (optional) fileName attribute specifies which file to take (relative + // to dependency-cruiser's current working directory. When not provided defaults + // to './webpack.conf.js'. + // + // The (optional) 'env' and 'arguments' attributes contain the parameters + // to be passed if your webpack config is a function and takes them (see + // webpack documentation for details) + // webpackConfig: { + // fileName: 'webpack.config.js', + // env: {}, + // arguments: {} + // }, + + // Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use + // for compilation + // babelConfig: { + // fileName: '.babelrc', + // }, + + // List of strings you have in use in addition to cjs/ es6 requires + // & imports to declare module dependencies. Use this e.g. if you've + // re-declared require, use a require-wrapper or use window.require as + // a hack. + // exoticRequireStrings: [], + + // options to pass on to enhanced-resolve, the package dependency-cruiser + // uses to resolve module references to disk. The values below should be + // suitable for most situations + // + // If you use webpack: you can also set these in webpack.conf.js. The set + // there will override the ones specified here. + enhancedResolveOptions: { + // What to consider as an 'exports' field in package.jsons + exportsFields: ['exports'], + + // List of conditions to check for in the exports field. + // Only works when the 'exportsFields' array is non-empty. + conditionNames: ['import', 'require', 'node', 'default', 'types'], + + // The extensions, by default are the same as the ones dependency-cruiser + // can access (run `npx depcruise --info` to see which ones that are in + // _your_ environment). If that list is larger than you need you can pass + // the extensions you actually use (e.g. ['.js', '.jsx']). This can speed + // up module resolution, which is the most expensive step. + // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"], + + // What to consider a 'main' field in package.json + mainFields: ["module", "main", "types", "typings"], + + // A list of alias fields in package.jsons + // See https://github.com/defunctzombie/package-browser-field-spec and + // the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) + // documentation. + // Defaults to an empty array (= don't use alias fields). + // aliasFields: ['browser'], + }, + + // skipAnalysisNotInRules will make dependency-cruiser execute + // analysis strictly necessary for checking the rule set only. + // See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#skipanalysisnotinrules + skipAnalysisNotInRules: true, + + reporterOptions: { + dot: { + // Pattern of modules to consolidate to. The default pattern in this configuration + // collapses everything in node_modules to one folder deep so you see + // the external modules, but not their innards. + collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + // Options to tweak the appearance of your graph. See + // https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions + // If you don't specify a theme dependency-cruiser falls back to a built-in one. + // theme: { + // graph: { + // // splines: 'ortho' - straight lines; slow on big graphs + // // splines: 'true' - bezier curves; fast but not as nice as ortho + // splines: 'true' + // }, + // }, + }, + archi: { + // Pattern of modules to consolidate to. + collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)', + + // Options to tweak the appearance of your graph. If you don't specify a + // theme for 'archi' dependency-cruiser will use the one specified in the + // dot section above and otherwise use the default one. + // theme: { }, + }, + text: { + highlightFocused: true + }, + } + } +}; +// generated: dependency-cruiser@17.3.6 on 2026-01-14T22:09:00.669Z diff --git a/packages/http-client-js/src/components/output.tsx b/packages/http-client-js/src/components/output.tsx index 5336221b9af..1f8a1a8d77c 100644 --- a/packages/http-client-js/src/components/output.tsx +++ b/packages/http-client-js/src/components/output.tsx @@ -1,4 +1,4 @@ -import { Children } from "@alloy-js/core/jsx-runtime"; +import { Children } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import { Program } from "@typespec/compiler"; import { Output as EFOutput, TransformNamePolicyContext } from "@typespec/emitter-framework"; diff --git a/packages/http-client-js/src/components/static-helpers/bytes-encoding.tsx b/packages/http-client-js/src/components/static-helpers/bytes-encoding.tsx index d994628a20f..0c31b3e8cf5 100644 --- a/packages/http-client-js/src/components/static-helpers/bytes-encoding.tsx +++ b/packages/http-client-js/src/components/static-helpers/bytes-encoding.tsx @@ -1,5 +1,4 @@ -import { code, Refkey, refkey } from "@alloy-js/core"; -import { Children } from "@alloy-js/core/jsx-runtime"; +import { Children, code, Refkey, refkey } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; export function getEncodeUint8ArrayRef(): Refkey { diff --git a/packages/http-client-js/src/components/transforms/json/json-transform.tsx b/packages/http-client-js/src/components/transforms/json/json-transform.tsx index edf4c78ceda..3faca211ce2 100644 --- a/packages/http-client-js/src/components/transforms/json/json-transform.tsx +++ b/packages/http-client-js/src/components/transforms/json/json-transform.tsx @@ -1,5 +1,4 @@ -import { code, Refkey } from "@alloy-js/core"; -import { Children } from "@alloy-js/core/jsx-runtime"; +import { Children, code, Refkey } from "@alloy-js/core"; import type { Type } from "@typespec/compiler"; import { useTsp } from "@typespec/emitter-framework"; import { ScalarDataTransform } from "../data-transform.jsx"; diff --git a/packages/http-client-js/src/components/transforms/multipart/array-part-transform.tsx b/packages/http-client-js/src/components/transforms/multipart/array-part-transform.tsx index b402dab172c..78bac56ba18 100644 --- a/packages/http-client-js/src/components/transforms/multipart/array-part-transform.tsx +++ b/packages/http-client-js/src/components/transforms/multipart/array-part-transform.tsx @@ -1,5 +1,4 @@ -import { code } from "@alloy-js/core"; -import { Children } from "@alloy-js/core/jsx-runtime"; +import { Children, code } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import { HttpOperationPart } from "@typespec/http"; import { HttpPartTransform } from "./part-transform.jsx"; From 37aec70b9ef107b0c70a5aa2d524878105d038f5 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Thu, 15 Jan 2026 14:23:09 -0800 Subject: [PATCH 3/8] Custom decl provider --- .../src/core/context/index.ts | 1 + .../src/core/declaration-provider.ts | 3 + packages/emitter-framework/src/core/index.ts | 1 + .../typescript/components/type-expression.tsx | 1 - .../src/typescript/utils/operation.ts | 9 ++- .../http-client-js/src/components/client.tsx | 1 + .../http-client-js/src/components/models.tsx | 11 ++- packages/http-client-js/src/emitter.tsx | 73 +++++++++++-------- .../http-client-js/src/utils/parameters.tsx | 6 +- packages/http-client-js/test.tsp | 48 +++--------- 10 files changed, 81 insertions(+), 73 deletions(-) diff --git a/packages/emitter-framework/src/core/context/index.ts b/packages/emitter-framework/src/core/context/index.ts index a4820a0403c..47ad8e0dcf2 100644 --- a/packages/emitter-framework/src/core/context/index.ts +++ b/packages/emitter-framework/src/core/context/index.ts @@ -1,2 +1,3 @@ +export * from "./declaration-provider.js"; export * from "./name-policy-context.js"; export * from "./tsp-context.js"; diff --git a/packages/emitter-framework/src/core/declaration-provider.ts b/packages/emitter-framework/src/core/declaration-provider.ts index bf2f69fe33b..419cbbf97d2 100644 --- a/packages/emitter-framework/src/core/declaration-provider.ts +++ b/packages/emitter-framework/src/core/declaration-provider.ts @@ -53,9 +53,11 @@ export class DeclarationProvider { * @throws if the type is not a declaration type. */ public getDeclarationRefkey(type: Type): Refkey { + console.log("Getting decl refkey for " + (type as any).name); const existing = this.declarations.get(type); if (existing) { + console.log("Existing: ", existing); return existing; } @@ -64,6 +66,7 @@ export class DeclarationProvider { } const key = refkey(); + console.log("New", key); this.declarations.set(type, key); return key; } diff --git a/packages/emitter-framework/src/core/index.ts b/packages/emitter-framework/src/core/index.ts index 6b3d6fb8a5a..8844ee0eb80 100644 --- a/packages/emitter-framework/src/core/index.ts +++ b/packages/emitter-framework/src/core/index.ts @@ -1,5 +1,6 @@ export * from "./components/index.js"; export * from "./context/index.js"; +export * from "./declaration-provider.js"; export * from "./scc-set.js"; export * from "./transport-name-policy.js"; export * from "./type-connector.js"; diff --git a/packages/emitter-framework/src/typescript/components/type-expression.tsx b/packages/emitter-framework/src/typescript/components/type-expression.tsx index 3308015c4be..87816ec3ac9 100644 --- a/packages/emitter-framework/src/typescript/components/type-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/type-expression.tsx @@ -32,7 +32,6 @@ export function TypeExpression(props: TypeExpressionProps) { {() => { const dp = useDeclarationProvider(); if (!props.noReference && dp.shouldReference(type)) { - console.log("Referencing " + type.kind); return dp.getRefkey(type); } diff --git a/packages/emitter-framework/src/typescript/utils/operation.ts b/packages/emitter-framework/src/typescript/utils/operation.ts index ae7a967f7f9..11285f965a1 100644 --- a/packages/emitter-framework/src/typescript/utils/operation.ts +++ b/packages/emitter-framework/src/typescript/utils/operation.ts @@ -47,7 +47,13 @@ export function buildParameterDescriptors( return allParams; } -export function buildParameterDescriptor(modelProperty: ModelProperty): ts.ParameterDescriptor { +/** + * Create a parameter descriptor from a model property type. + */ +export function buildParameterDescriptor( + modelProperty: ModelProperty, + options: { refkey?: Refkey } = {}, +): ts.ParameterDescriptor { const { $ } = useTsp(); const namePolicy = ts.useTSNamePolicy(); const paramName = namePolicy.getName(modelProperty.name, "parameter"); @@ -58,6 +64,7 @@ export function buildParameterDescriptor(modelProperty: ModelProperty): ts.Param name: paramName, optional: isOptional, type: TypeExpression({ type: modelProperty.type }), + refkey: options.refkey, }; } diff --git a/packages/http-client-js/src/components/client.tsx b/packages/http-client-js/src/components/client.tsx index 3c3beceee37..cf1f7812162 100644 --- a/packages/http-client-js/src/components/client.tsx +++ b/packages/http-client-js/src/components/client.tsx @@ -70,6 +70,7 @@ export function ClientClass(props: ClientClassProps) { {(op) => { const parameters = getOperationParameters(op.httpOperation, refkey()); + console.log("Params: ", parameters); const args = parameters.flatMap((p) => p.refkey); const isPaging = Boolean($.operation.getPagingMetadata(op.httpOperation.operation)); diff --git a/packages/http-client-js/src/components/models.tsx b/packages/http-client-js/src/components/models.tsx index 8c165096b40..f891d699c76 100644 --- a/packages/http-client-js/src/components/models.tsx +++ b/packages/http-client-js/src/components/models.tsx @@ -1,6 +1,6 @@ -import { For, refkey } from "@alloy-js/core"; +import { For } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; -import { useTsp } from "@typespec/emitter-framework"; +import { useDeclarationProvider, useTsp } from "@typespec/emitter-framework"; import * as ef from "@typespec/emitter-framework/typescript"; import { useClientLibrary } from "@typespec/http-client"; @@ -12,12 +12,17 @@ export function Models(props: ModelsProps) { const { $ } = useTsp(); const clientLibrary = useClientLibrary(); const dataTypes = clientLibrary.dataTypes; + const dp = useDeclarationProvider(); return ( {(type) => { + if (!dp.isDeclaration(type)) { + return; + } + return $.array.is(type) || $.record.is(type) ? null : ( - + ); }} diff --git a/packages/http-client-js/src/emitter.tsx b/packages/http-client-js/src/emitter.tsx index c23a3cd54db..96975a40bfd 100644 --- a/packages/http-client-js/src/emitter.tsx +++ b/packages/http-client-js/src/emitter.tsx @@ -1,7 +1,10 @@ import { Children, SourceDirectory } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; -import { EmitContext } from "@typespec/compiler"; +import { EmitContext, Type } from "@typespec/compiler"; +import { $ } from "@typespec/compiler/typekit"; import { + DeclarationProvider, + DeclarationProviderContext, Experimental_ComponentOverrides, Experimental_ComponentOverridesConfig, useTsp, @@ -25,39 +28,42 @@ import { JsClientEmitterOptions } from "./lib.js"; */ export async function $onEmit(context: EmitContext) { const packageName = context.options["package-name"] ?? "test-package"; + const dp = new HttpClientJsDeclarationProvider($(context.program)); const output = ( - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + ); @@ -82,3 +88,12 @@ export function HttpClientOverrides(props: { children?: Children }) { ); } + +/** + * Custom declaration provider which treats everything with a name as a declaration. + */ +class HttpClientJsDeclarationProvider extends DeclarationProvider { + override isDeclaration(type: Type): boolean { + return "name" in type && type.name !== undefined && type.name !== ""; + } +} diff --git a/packages/http-client-js/src/utils/parameters.tsx b/packages/http-client-js/src/utils/parameters.tsx index 076e4ccdeea..a6eef170df5 100644 --- a/packages/http-client-js/src/utils/parameters.tsx +++ b/packages/http-client-js/src/utils/parameters.tsx @@ -49,7 +49,7 @@ function buildClientParameterDescriptor( ): ts.ParameterDescriptor | undefined { const { $ } = useTsp(); const authSchemes = $.modelProperty.getCredentialAuth(modelProperty); - + const key = refkey(modelProperty, suffixRefkey); if (authSchemes) { if (authSchemes.length === 1 && authSchemes[0].type === "noAuth") { return undefined; @@ -60,7 +60,7 @@ function buildClientParameterDescriptor( ); return { name: "credential", - refkey: refkey(modelProperty, suffixRefkey), + refkey: key, optional: modelProperty.optional, type: mapJoin( () => credentialType, @@ -70,7 +70,7 @@ function buildClientParameterDescriptor( }; } - return buildParameterDescriptor(modelProperty); + return buildParameterDescriptor(modelProperty, { refkey: key }); } const oauth2FlowRefs: Record = { diff --git a/packages/http-client-js/test.tsp b/packages/http-client-js/test.tsp index 82fe8a0246f..f004cebe878 100644 --- a/packages/http-client-js/test.tsp +++ b/packages/http-client-js/test.tsp @@ -2,39 +2,15 @@ import "@typespec/http"; using Http; @service(#{ title: "Widget Service" }) -namespace DemoService; - -model Widget { - @visibility(Lifecycle.Read) - id: string; - - weight: int32; - color: "red" | "blue"; -} - -@error -model Error { - code: int32; - message: string; -} - -@route("/widgets") -@tag("Widgets") -interface Widgets { - /** List widgets */ - @get list(): Widget[] | Error; - - /** Read widgets */ - @get read(@path id: Widget.id): Widget | Error; - - /** Create a widget */ - @post create(@body widget: Widget): Widget | Error; - - /** Update a widget */ - @patch update(@path id: Widget.id, @body widget: MergePatchUpdate): Widget | Error; - - /** Delete a widget */ - @delete delete(@path id: Widget.id): void | Error; - /** Analyze a widget */ - @route("{id}/analyze") @post analyze(@path id: Widget.id): string | Error; -} +namespace Test; +@post +@route("/non-string-float") +op float( + @header contentType: "multipart/form-data", + @multipartBody body: { + temperature: HttpPart<{ + @body body: float64; + @header contentType: "text/plain"; + }>; + }, +): NoContentResponse; From 51e464282054abe2377d0638aaeac6aa63149253 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Thu, 15 Jan 2026 15:38:00 -0800 Subject: [PATCH 4/8] Tests pass --- .../src/core/declaration-provider.ts | 10 ++- .../http-client-js/src/components/client.tsx | 1 - .../http-client-js/src/components/models.tsx | 4 +- .../transforms/json/json-model-transform.tsx | 7 +- .../json/json-transform-discriminator.tsx | 6 +- .../transforms/json/union-transform.tsx | 6 +- packages/http-client-js/src/emitter.tsx | 73 ++++++++----------- packages/http-client-js/test.tsp | 15 +--- .../scenarios/additional-properties/is.md | 5 +- .../test/scenarios/serializers/scalars.md | 8 -- 10 files changed, 51 insertions(+), 84 deletions(-) diff --git a/packages/emitter-framework/src/core/declaration-provider.ts b/packages/emitter-framework/src/core/declaration-provider.ts index 419cbbf97d2..f70d96688d5 100644 --- a/packages/emitter-framework/src/core/declaration-provider.ts +++ b/packages/emitter-framework/src/core/declaration-provider.ts @@ -1,5 +1,5 @@ import { refkey, shallowReactive, type Refkey } from "@alloy-js/core"; -import { getLocationContext, type MemberType, type Type } from "@typespec/compiler"; +import { getLocationContext, isVoidType, type MemberType, type Type } from "@typespec/compiler"; import type { Typekit } from "@typespec/compiler/typekit"; export class DeclarationProvider { @@ -53,11 +53,9 @@ export class DeclarationProvider { * @throws if the type is not a declaration type. */ public getDeclarationRefkey(type: Type): Refkey { - console.log("Getting decl refkey for " + (type as any).name); const existing = this.declarations.get(type); if (existing) { - console.log("Existing: ", existing); return existing; } @@ -66,7 +64,6 @@ export class DeclarationProvider { } const key = refkey(); - console.log("New", key); this.declarations.set(type, key); return key; } @@ -90,6 +87,11 @@ export class DeclarationProvider { return false; } + // BUG: intrinsic.is doesn't capture void... + if (this.$.intrinsic.is(type) || isVoidType(type)) { + return false; + } + if (!("name" in type) || type.name === undefined || type.name === "") { return false; } diff --git a/packages/http-client-js/src/components/client.tsx b/packages/http-client-js/src/components/client.tsx index cf1f7812162..3c3beceee37 100644 --- a/packages/http-client-js/src/components/client.tsx +++ b/packages/http-client-js/src/components/client.tsx @@ -70,7 +70,6 @@ export function ClientClass(props: ClientClassProps) { {(op) => { const parameters = getOperationParameters(op.httpOperation, refkey()); - console.log("Params: ", parameters); const args = parameters.flatMap((p) => p.refkey); const isPaging = Boolean($.operation.getPagingMetadata(op.httpOperation.operation)); diff --git a/packages/http-client-js/src/components/models.tsx b/packages/http-client-js/src/components/models.tsx index f891d699c76..77c287a9bdc 100644 --- a/packages/http-client-js/src/components/models.tsx +++ b/packages/http-client-js/src/components/models.tsx @@ -21,9 +21,7 @@ export function Models(props: ModelsProps) { return; } - return $.array.is(type) || $.record.is(type) ? null : ( - - ); + return ; }} diff --git a/packages/http-client-js/src/components/transforms/json/json-model-transform.tsx b/packages/http-client-js/src/components/transforms/json/json-model-transform.tsx index 284fb98572f..84338faaab4 100644 --- a/packages/http-client-js/src/components/transforms/json/json-model-transform.tsx +++ b/packages/http-client-js/src/components/transforms/json/json-model-transform.tsx @@ -2,7 +2,7 @@ import * as ts from "@alloy-js/typescript"; import { type Children, code, For, type Refkey, refkey } from "@alloy-js/core"; import type { Model } from "@typespec/compiler"; -import { useTsp } from "@typespec/emitter-framework"; +import { useDeclarationProvider, useTsp } from "@typespec/emitter-framework"; import { JsonAdditionalPropertiesTransform } from "./json-model-additional-properties-transform.jsx"; import { JsonModelPropertyTransform } from "./json-model-property-transform.jsx"; import { JsonRecordTransformDeclaration } from "./json-record-transform.jsx"; @@ -70,13 +70,14 @@ export interface JsonModelTransformDeclarationProps { export function JsonModelTransformDeclaration(props: JsonModelTransformDeclarationProps): Children { const { $ } = useTsp(); const namePolicy = ts.useTSNamePolicy(); + const dp = useDeclarationProvider(); const transformName = namePolicy.getName( `json_${props.type.name}_to_${props.target}_transform`, "function", ); - const returnType = props.target === "transport" ? "any" : refkey(props.type); - const inputType = props.target === "transport" ? <>{refkey(props.type)} | null : "any"; + const returnType = props.target === "transport" ? "any" : dp.getRefkey(props.type); + const inputType = props.target === "transport" ? <>{dp.getRefkey(props.type)} | null : "any"; const inputRef = refkey(); const parameters: ts.ParameterDescriptor[] = [ diff --git a/packages/http-client-js/src/components/transforms/json/json-transform-discriminator.tsx b/packages/http-client-js/src/components/transforms/json/json-transform-discriminator.tsx index ffb0e656ce9..0ced5d707d5 100644 --- a/packages/http-client-js/src/components/transforms/json/json-transform-discriminator.tsx +++ b/packages/http-client-js/src/components/transforms/json/json-transform-discriminator.tsx @@ -1,7 +1,7 @@ import { code, mapJoin, refkey, useNamePolicy, type Children, type Refkey } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import { Discriminator, Model, Union } from "@typespec/compiler"; -import { useTsp } from "@typespec/emitter-framework"; +import { useDeclarationProvider, useTsp } from "@typespec/emitter-framework"; import { JsonTransform } from "./json-transform.jsx"; export interface JsonTransformDiscriminatorProps { @@ -82,8 +82,8 @@ export function JsonTransformDiscriminatorDeclaration( `json_${props.type.name}_to_${props.target}_discriminator`, "function", ); - - const typeRef = refkey(props.type); + const dp = useDeclarationProvider(); + const typeRef = dp.getRefkey(props.type); const returnType = props.target === "transport" ? "any" : typeRef; const inputType = props.target === "transport" ? typeRef : "any"; const inputRef = refkey(); diff --git a/packages/http-client-js/src/components/transforms/json/union-transform.tsx b/packages/http-client-js/src/components/transforms/json/union-transform.tsx index d546cf021a9..3ddc403681a 100644 --- a/packages/http-client-js/src/components/transforms/json/union-transform.tsx +++ b/packages/http-client-js/src/components/transforms/json/union-transform.tsx @@ -1,7 +1,7 @@ import { Children, code, refkey, Refkey } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import type { Union } from "@typespec/compiler"; -import { useTsp } from "@typespec/emitter-framework"; +import { useDeclarationProvider, useTsp } from "@typespec/emitter-framework"; import { getJsonTransformDiscriminatorRefkey, JsonTransformDiscriminatorDeclaration, @@ -53,8 +53,8 @@ export function JsonUnionTransformDeclaration(props: JsonUnionTransformDeclarati `json_${props.type.name}_to_${props.target}_transform`, "function", ); - - const typeRef = refkey(props.type); + const dp = useDeclarationProvider(); + const typeRef = dp.getRefkey(props.type); const returnType = props.target === "transport" ? "any" : typeRef; const inputType = props.target === "transport" ? <>{typeRef} | null : "any"; const inputRef = refkey(); diff --git a/packages/http-client-js/src/emitter.tsx b/packages/http-client-js/src/emitter.tsx index 96975a40bfd..c23a3cd54db 100644 --- a/packages/http-client-js/src/emitter.tsx +++ b/packages/http-client-js/src/emitter.tsx @@ -1,10 +1,7 @@ import { Children, SourceDirectory } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; -import { EmitContext, Type } from "@typespec/compiler"; -import { $ } from "@typespec/compiler/typekit"; +import { EmitContext } from "@typespec/compiler"; import { - DeclarationProvider, - DeclarationProviderContext, Experimental_ComponentOverrides, Experimental_ComponentOverridesConfig, useTsp, @@ -28,42 +25,39 @@ import { JsClientEmitterOptions } from "./lib.js"; */ export async function $onEmit(context: EmitContext) { const packageName = context.options["package-name"] ?? "test-package"; - const dp = new HttpClientJsDeclarationProvider($(context.program)); const output = ( - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - + + + + + + + + + + + + + ); @@ -88,12 +82,3 @@ export function HttpClientOverrides(props: { children?: Children }) { ); } - -/** - * Custom declaration provider which treats everything with a name as a declaration. - */ -class HttpClientJsDeclarationProvider extends DeclarationProvider { - override isDeclaration(type: Type): boolean { - return "name" in type && type.name !== undefined && type.name !== ""; - } -} diff --git a/packages/http-client-js/test.tsp b/packages/http-client-js/test.tsp index f004cebe878..07f91be1dfb 100644 --- a/packages/http-client-js/test.tsp +++ b/packages/http-client-js/test.tsp @@ -3,14 +3,7 @@ import "@typespec/http"; using Http; @service(#{ title: "Widget Service" }) namespace Test; -@post -@route("/non-string-float") -op float( - @header contentType: "multipart/form-data", - @multipartBody body: { - temperature: HttpPart<{ - @body body: float64; - @header contentType: "text/plain"; - }>; - }, -): NoContentResponse; + +model Widget is Record; + +op foo(): Widget; diff --git a/packages/http-client-js/test/scenarios/additional-properties/is.md b/packages/http-client-js/test/scenarios/additional-properties/is.md index b1ebf098998..f51cf726383 100644 --- a/packages/http-client-js/test/scenarios/additional-properties/is.md +++ b/packages/http-client-js/test/scenarios/additional-properties/is.md @@ -21,10 +21,7 @@ Should not create model and treat it as a Record. Should just treat it as a Record ```ts src/api/testClientOperations.ts function foo -export async function foo( - client: TestClientContext, - options?: FooOptions, -): Promise> { +export async function foo(client: TestClientContext, options?: FooOptions): Promise { const path = parse("/").expand({}); const httpRequestOptions = { headers: {}, diff --git a/packages/http-client-js/test/scenarios/serializers/scalars.md b/packages/http-client-js/test/scenarios/serializers/scalars.md index d3d3c871b52..c888252596c 100644 --- a/packages/http-client-js/test/scenarios/serializers/scalars.md +++ b/packages/http-client-js/test/scenarios/serializers/scalars.md @@ -19,15 +19,7 @@ op foo(a: MyDate, b: MyUtcDate, c: MyIsoDate, d: MyUnixDate): void; ## TypeScript ```ts src/models/models.ts -/** - * A sequence of textual characters. - */ -export type String = string; export type MyDate = Date; -/** - * An instant in coordinated universal time (UTC)" - */ -export type UtcDateTime = Date; export type MyUtcDate = Date; export type MyIsoDate = Date; export type MyUnixDate = Date; From 99dfcbfe30d07c785605c7e9ece5e3125450dd60 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Thu, 15 Jan 2026 15:46:29 -0800 Subject: [PATCH 5/8] cleanup --- .../decl-provider-2026-0-15-15-41-29.md | 7 + .../decl-provider-2026-0-15-15-43-16.md | 7 + .../decl-provider-2026-0-15-15-43-33.md | 7 + .../decl-provider-2026-0-15-15-44-18.md | 7 + .../decl-provider-2026-0-15-15-45-2.md | 7 + .../http-client-js/.dependency-cruiser.cjs | 382 ------------------ .../http-client-js/src/components/models.tsx | 3 +- 7 files changed, 36 insertions(+), 384 deletions(-) create mode 100644 .chronus/changes/decl-provider-2026-0-15-15-41-29.md create mode 100644 .chronus/changes/decl-provider-2026-0-15-15-43-16.md create mode 100644 .chronus/changes/decl-provider-2026-0-15-15-43-33.md create mode 100644 .chronus/changes/decl-provider-2026-0-15-15-44-18.md create mode 100644 .chronus/changes/decl-provider-2026-0-15-15-45-2.md delete mode 100644 packages/http-client-js/.dependency-cruiser.cjs diff --git a/.chronus/changes/decl-provider-2026-0-15-15-41-29.md b/.chronus/changes/decl-provider-2026-0-15-15-41-29.md new file mode 100644 index 00000000000..6c10ed85c8e --- /dev/null +++ b/.chronus/changes/decl-provider-2026-0-15-15-41-29.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/emitter-framework" +--- + +Add DeclarationProvider, a mechanism to reactively track declarations discovered as they are emitted. \ No newline at end of file diff --git a/.chronus/changes/decl-provider-2026-0-15-15-43-16.md b/.chronus/changes/decl-provider-2026-0-15-15-43-16.md new file mode 100644 index 00000000000..a656c1a38fa --- /dev/null +++ b/.chronus/changes/decl-provider-2026-0-15-15-43-16.md @@ -0,0 +1,7 @@ +--- +changeKind: breaking +packages: + - "@typespec/emitter-framework" +--- + +TypeScript: Adopt DeclarationProvider, which changes how refkeys to declarations are assigned and referenced. The current provider vends fresh refkeys when needed, so `refkey(type)` is no longer a viable way to refer to declarations created by the emitter framework. Replace all forms of `refkey(type)` with `dp.getRefkey(type)`. \ No newline at end of file diff --git a/.chronus/changes/decl-provider-2026-0-15-15-43-33.md b/.chronus/changes/decl-provider-2026-0-15-15-43-33.md new file mode 100644 index 00000000000..04544234568 --- /dev/null +++ b/.chronus/changes/decl-provider-2026-0-15-15-43-33.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-js" +--- + +Adopt EF changes. \ No newline at end of file diff --git a/.chronus/changes/decl-provider-2026-0-15-15-44-18.md b/.chronus/changes/decl-provider-2026-0-15-15-44-18.md new file mode 100644 index 00000000000..e8885b6a213 --- /dev/null +++ b/.chronus/changes/decl-provider-2026-0-15-15-44-18.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-js" +--- + +Don't emit types for built-ins like floats and such (which were unused anyway). \ No newline at end of file diff --git a/.chronus/changes/decl-provider-2026-0-15-15-45-2.md b/.chronus/changes/decl-provider-2026-0-15-15-45-2.md new file mode 100644 index 00000000000..ccb4bf6cc52 --- /dev/null +++ b/.chronus/changes/decl-provider-2026-0-15-15-45-2.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-js" +--- + +Emit types for model is `Record`. \ No newline at end of file diff --git a/packages/http-client-js/.dependency-cruiser.cjs b/packages/http-client-js/.dependency-cruiser.cjs deleted file mode 100644 index 190a31dae9e..00000000000 --- a/packages/http-client-js/.dependency-cruiser.cjs +++ /dev/null @@ -1,382 +0,0 @@ -/** @type {import('dependency-cruiser').IConfiguration} */ -module.exports = { - forbidden: [ - { - name: 'no-circular', - severity: 'warn', - comment: - "This dependency is part of a circular relationship. You might want to revise " + - "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ", - from: {}, - to: { - circular: true - } - }, - { - name: 'no-orphans', - comment: - "This is an orphan module - it's likely not used (anymore?). Either use it or " + - "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + - "add an exception for it in your dependency-cruiser configuration. By default " + - "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + - "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", - severity: 'warn', - from: { - orphan: true, - pathNot: [ - '(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files - '[.]d[.]ts$', // TypeScript declaration files - '(^|/)tsconfig[.]json$', // TypeScript config - '(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs - ] - }, - to: {}, - }, - { - name: 'no-deprecated-core', - comment: - 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + - "bound to exist - node doesn't deprecate lightly.", - severity: 'warn', - from: {}, - to: { - dependencyTypes: [ - 'core' - ], - path: [ - '^v8/tools/codemap$', - '^v8/tools/consarray$', - '^v8/tools/csvparser$', - '^v8/tools/logreader$', - '^v8/tools/profile_view$', - '^v8/tools/profile$', - '^v8/tools/SourceMap$', - '^v8/tools/splaytree$', - '^v8/tools/tickprocessor-driver$', - '^v8/tools/tickprocessor$', - '^node-inspect/lib/_inspect$', - '^node-inspect/lib/internal/inspect_client$', - '^node-inspect/lib/internal/inspect_repl$', - '^async_hooks$', - '^punycode$', - '^domain$', - '^constants$', - '^sys$', - '^_linklist$', - '^_stream_wrap$' - ], - } - }, - { - name: 'not-to-deprecated', - comment: - 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + - 'version of that module, or find an alternative. Deprecated modules are a security risk.', - severity: 'warn', - from: {}, - to: { - dependencyTypes: [ - 'deprecated' - ] - } - }, - { - name: 'no-non-package-json', - severity: 'error', - comment: - "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + - "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + - "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + - "in your package.json.", - from: {}, - to: { - dependencyTypes: [ - 'npm-no-pkg', - 'npm-unknown' - ] - } - }, - { - name: 'not-to-unresolvable', - comment: - "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + - 'module: add it to your package.json. In all other cases you likely already know what to do.', - severity: 'error', - from: {}, - to: { - couldNotResolve: true - } - }, - { - name: 'no-duplicate-dep-types', - comment: - "Likely this module depends on an external ('npm') package that occurs more than once " + - "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + - "maintenance problems later on.", - severity: 'warn', - from: {}, - to: { - moreThanOneDependencyType: true, - // as it's common to use a devDependency for type-only imports: don't - // consider type-only dependencyTypes for this rule - dependencyTypesNot: ["type-only"] - } - }, - - // rules you might want to tweak for your specific situation: - { - name: 'not-to-test', - comment: - "This module depends on code within a folder that should only contain tests. As tests don't " + - "implement functionality this is odd. Either you're writing a test outside the test folder " + - "or there's something in the test folder that isn't a test.", - severity: 'error', - from: { - pathNot: '^(test)' - }, - to: { - path: '^(test)' - } - }, - { - name: 'not-to-spec', - comment: - 'This module depends on a spec (test) file. The responsibility of a spec file is to test code. ' + - "If there's something in a spec that's of use to other modules, it doesn't have that single " + - 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', - severity: 'error', - from: {}, - to: { - path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' - } - }, - { - name: 'not-to-dev-dep', - severity: 'error', - comment: - "This module depends on an npm package from the 'devDependencies' section of your " + - 'package.json. It looks like something that ships to production, though. To prevent problems ' + - "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + - 'section of your package.json. If this module is development only - add it to the ' + - 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', - from: { - path: '^(src)', - pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' - }, - to: { - dependencyTypes: [ - 'npm-dev', - ], - // type only dependencies are not a problem as they don't end up in the - // production code or are ignored by the runtime. - dependencyTypesNot: [ - 'type-only' - ], - pathNot: [ - 'node_modules/@types/' - ] - } - }, - { - name: 'optional-deps-used', - severity: 'info', - comment: - "This module depends on an npm package that is declared as an optional dependency " + - "in your package.json. As this makes sense in limited situations only, it's flagged here. " + - "If you use an optional dependency here by design - add an exception to your" + - "dependency-cruiser configuration.", - from: {}, - to: { - dependencyTypes: [ - 'npm-optional' - ] - } - }, - { - name: 'peer-deps-used', - comment: - "This module depends on an npm package that is declared as a peer dependency " + - "in your package.json. This makes sense if your package is e.g. a plugin, but in " + - "other cases - maybe not so much. If the use of a peer dependency is intentional " + - "add an exception to your dependency-cruiser configuration.", - severity: 'warn', - from: {}, - to: { - dependencyTypes: [ - 'npm-peer' - ] - } - } - ], - options: { - // Which modules not to follow further when encountered - doNotFollow: { - // path: an array of regular expressions in strings to match against - path: ['node_modules'] - }, - - // Which modules to exclude - // exclude : { - // // path: an array of regular expressions in strings to match against - // path: '', - // }, - - // Which modules to exclusively include (array of regular expressions in strings) - // dependency-cruiser will skip everything that doesn't match this pattern - // includeOnly : [''], - - // List of module systems to cruise. - // When left out dependency-cruiser will fall back to the list of _all_ - // module systems it knows of ('amd', 'cjs', 'es6', 'tsd']). It's the - // default because it's the safe option. It comes at a performance penalty, though - // As in practice only commonjs ('cjs') and ecmascript modules ('es6') - // are in wide use, you can limit the moduleSystems to those. - // moduleSystems: ['cjs', 'es6'], - - // false: don't look at JSDoc imports (the default) - // true: detect dependencies in JSDoc-style import statements. - // Implies parser: 'tsc', which a.o. means the typescript compiler will need - // to be installed in the same spot you run dependency-cruiser from. - // detectJSDocImports: true, - - // false: don't look at process.getBuiltinModule calls (the default) - // true: dependency-cruiser will detect calls to process.getBuiltinModule/ - // globalThis.process.getBuiltinModule as imports. - detectProcessBuiltinModuleCalls: true, - - // prefix for links in html, d2, mermaid and dot/ svg output (e.g. 'https://github.com/you/yourrepo/blob/main/' - // to open it on your online repo or `vscode://file/${process.cwd()}/` to - // open it in visual studio code), - // prefix: `vscode://file/${process.cwd()}/`, - - // suffix for links in output. E.g. put .html here if you use it to link to - // your coverage reports. - // suffix: '.html', - - // false (the default): ignore dependencies that only exist before typescript-to-javascript compilation - // true: also detect dependencies that only exist before typescript-to-javascript compilation - // 'specify': for each dependency identify whether it only exists before compilation or also after - tsPreCompilationDeps: true, - - // list of extensions to scan that aren't javascript or compile-to-javascript. - // Empty by default. Only put extensions in here that you want to take into - // account that are _not_ parsable. - // extraExtensionsToScan: ['.json', '.jpg', '.png', '.svg', '.webp'], - - // if true combines the package.jsons found from the module up to the base - // folder the cruise is initiated from. Useful for how (some) mono-repos - // manage dependencies & dependency definitions. - // combinedDependencies: false, - - // if true leave symlinks untouched, otherwise use the realpath - // preserveSymlinks: false, - - // TypeScript project file ('tsconfig.json') to use for - // (1) compilation and - // (2) resolution (e.g. with the paths property) - // - // The (optional) fileName attribute specifies which file to take (relative to - // dependency-cruiser's current working directory). When not provided - // defaults to './tsconfig.json'. - tsConfig: { - fileName: 'tsconfig.json' - }, - - // Webpack configuration to use to get resolve options from. - // - // The (optional) fileName attribute specifies which file to take (relative - // to dependency-cruiser's current working directory. When not provided defaults - // to './webpack.conf.js'. - // - // The (optional) 'env' and 'arguments' attributes contain the parameters - // to be passed if your webpack config is a function and takes them (see - // webpack documentation for details) - // webpackConfig: { - // fileName: 'webpack.config.js', - // env: {}, - // arguments: {} - // }, - - // Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use - // for compilation - // babelConfig: { - // fileName: '.babelrc', - // }, - - // List of strings you have in use in addition to cjs/ es6 requires - // & imports to declare module dependencies. Use this e.g. if you've - // re-declared require, use a require-wrapper or use window.require as - // a hack. - // exoticRequireStrings: [], - - // options to pass on to enhanced-resolve, the package dependency-cruiser - // uses to resolve module references to disk. The values below should be - // suitable for most situations - // - // If you use webpack: you can also set these in webpack.conf.js. The set - // there will override the ones specified here. - enhancedResolveOptions: { - // What to consider as an 'exports' field in package.jsons - exportsFields: ['exports'], - - // List of conditions to check for in the exports field. - // Only works when the 'exportsFields' array is non-empty. - conditionNames: ['import', 'require', 'node', 'default', 'types'], - - // The extensions, by default are the same as the ones dependency-cruiser - // can access (run `npx depcruise --info` to see which ones that are in - // _your_ environment). If that list is larger than you need you can pass - // the extensions you actually use (e.g. ['.js', '.jsx']). This can speed - // up module resolution, which is the most expensive step. - // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"], - - // What to consider a 'main' field in package.json - mainFields: ["module", "main", "types", "typings"], - - // A list of alias fields in package.jsons - // See https://github.com/defunctzombie/package-browser-field-spec and - // the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields) - // documentation. - // Defaults to an empty array (= don't use alias fields). - // aliasFields: ['browser'], - }, - - // skipAnalysisNotInRules will make dependency-cruiser execute - // analysis strictly necessary for checking the rule set only. - // See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#skipanalysisnotinrules - skipAnalysisNotInRules: true, - - reporterOptions: { - dot: { - // Pattern of modules to consolidate to. The default pattern in this configuration - // collapses everything in node_modules to one folder deep so you see - // the external modules, but not their innards. - collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)', - - // Options to tweak the appearance of your graph. See - // https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions - // If you don't specify a theme dependency-cruiser falls back to a built-in one. - // theme: { - // graph: { - // // splines: 'ortho' - straight lines; slow on big graphs - // // splines: 'true' - bezier curves; fast but not as nice as ortho - // splines: 'true' - // }, - // }, - }, - archi: { - // Pattern of modules to consolidate to. - collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)', - - // Options to tweak the appearance of your graph. If you don't specify a - // theme for 'archi' dependency-cruiser will use the one specified in the - // dot section above and otherwise use the default one. - // theme: { }, - }, - text: { - highlightFocused: true - }, - } - } -}; -// generated: dependency-cruiser@17.3.6 on 2026-01-14T22:09:00.669Z diff --git a/packages/http-client-js/src/components/models.tsx b/packages/http-client-js/src/components/models.tsx index 77c287a9bdc..1b5ccb8efe1 100644 --- a/packages/http-client-js/src/components/models.tsx +++ b/packages/http-client-js/src/components/models.tsx @@ -1,6 +1,6 @@ import { For } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; -import { useDeclarationProvider, useTsp } from "@typespec/emitter-framework"; +import { useDeclarationProvider } from "@typespec/emitter-framework"; import * as ef from "@typespec/emitter-framework/typescript"; import { useClientLibrary } from "@typespec/http-client"; @@ -9,7 +9,6 @@ export interface ModelsProps { } export function Models(props: ModelsProps) { - const { $ } = useTsp(); const clientLibrary = useClientLibrary(); const dataTypes = clientLibrary.dataTypes; const dp = useDeclarationProvider(); From cef384a323b3efdc024f81eda8ad39dbe268340e Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Thu, 15 Jan 2026 15:51:35 -0800 Subject: [PATCH 6/8] more changes --- .../decl-provider-2026-0-15-15-49-58.md | 7 + .../decl-provider-2026-0-15-15-51-26.md | 7 + .../src/core/custom-emit.tss | 122 ------------------ 3 files changed, 14 insertions(+), 122 deletions(-) create mode 100644 .chronus/changes/decl-provider-2026-0-15-15-49-58.md create mode 100644 .chronus/changes/decl-provider-2026-0-15-15-51-26.md delete mode 100644 packages/emitter-framework/src/core/custom-emit.tss diff --git a/.chronus/changes/decl-provider-2026-0-15-15-49-58.md b/.chronus/changes/decl-provider-2026-0-15-15-49-58.md new file mode 100644 index 00000000000..2e9f058effb --- /dev/null +++ b/.chronus/changes/decl-provider-2026-0-15-15-49-58.md @@ -0,0 +1,7 @@ +--- +changeKind: breaking +packages: + - "@typespec/emitter-framework" +--- + +TypeScript: remove notion of suffix refkey. Since its purpose is to create a unique refkey based on the suffix, users can just provide such a refkey instead of providing the suffix. \ No newline at end of file diff --git a/.chronus/changes/decl-provider-2026-0-15-15-51-26.md b/.chronus/changes/decl-provider-2026-0-15-15-51-26.md new file mode 100644 index 00000000000..2616cc9e70f --- /dev/null +++ b/.chronus/changes/decl-provider-2026-0-15-15-51-26.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/emitter-framework" +--- + +Add `refkey` option to `buildParameterDescriptor`. \ No newline at end of file diff --git a/packages/emitter-framework/src/core/custom-emit.tss b/packages/emitter-framework/src/core/custom-emit.tss deleted file mode 100644 index b564af13121..00000000000 --- a/packages/emitter-framework/src/core/custom-emit.tss +++ /dev/null @@ -1,122 +0,0 @@ -import { Refkey } from "@alloy-js/core"; -import { Children, ComponentDefinition } from "@alloy-js/core/jsx-runtime"; -import { Enum, EnumMember, ModelProperty, Program, Scalar, Type } from "@typespec/compiler"; -import { UnionVariant } from "@typespec/compiler/src/core/types"; -import { $ } from "@typespec/compiler/typekit"; - -const getEmitOptionsForTypeSym: unique symbol = Symbol.for( - "@typespec/emitter-framework:getEmitOptionsForType", -); - -const getEmitOptionsForTypeKindSym: unique symbol = Symbol.for( - "@typespec/emitter-framework:getEmitOptionsForTypeKind", -); - -export function typeEmitOptions() { - return new TypeEmitOptionsBuilder(); -} - -typeEmitOptions().forTypeKind("Enum", { - declare(props) { - props.type; - }, -}); - -declare const foo: Enum; -typeEmitOptions().forType(foo, { - declare(props) { - props.type; - }, -}); -export class TypeEmitOptionsBuilder { - #typeEmitOptions: Map> = new Map(); - #typeKindEmitOptions: Map> = new Map(); - - forType(type: T, options: TypeEmitOptions) { - this.#typeEmitOptions.set(type, options); - - return this; - } - - forTypeKind( - typeKind: TKind, - options: TypeEmitOptions>, - ) { - this.#typeKindEmitOptions.set(typeKind, options); - - return this; - } - - /** - * @internal - */ - [getEmitOptionsForTypeSym](program: Program, type: Type) { - let options = this.#typeEmitOptions.get(type); - if (options || !$(program).scalar.is(type) || program.checker.isStdType(type)) { - return options; - } - - // have a scalar, it's not a built-in scalar, and didn't find options, so - // see if we have options for a base scalar. - let currentScalar: Scalar | undefined = type; - while ( - currentScalar && - !program.checker.isStdType(type) && - !this.#typeEmitOptions.has(currentScalar) - ) { - currentScalar = currentScalar?.baseScalar; - } - - if (!currentScalar) { - return undefined; - } - - return this.#typeEmitOptions.get(currentScalar); - } - - /** - * @internal - */ - [getEmitOptionsForTypeKindSym](program: Program, typeKind: Type["kind"]) { - return this.#typeKindEmitOptions.get(typeKind); - } -} - -interface TypeEmitOptionsDeclarationDefaultProps { - name: string; - refkey: Refkey | Refkey[]; -} - -interface TypeEmitOptionsReferenceDefaultProps {} - -interface TypeEmitOptionsDeclarationProps { - type: TType; - default: Children; - defaultProps: TypeEmitOptionsDeclarationDefaultProps; -} - -interface TypeEmitOptionsExpressionProps { - type: TType; - default: Children; - defaultProps: TypeEmitOptionsReferenceDefaultProps; - member?: UnionVariant | EnumMember | ModelProperty; -} - -interface TypeEmitOptions< - TType extends Type, - TDeclarationProps = TypeEmitOptionsDeclarationProps, - TExpressionProps = TypeEmitOptionsExpressionProps, - TReferenceProps = TypeEmitOptionsReferenceProps, -> { - declaration?: ComponentDefinition; - expression?: ComponentDefinition; - reference?: ComponentDefinition; - noDeclaration?: boolean; - noReference?: boolean; - noExpression?: boolean; -} - -typeEmitOptions().forTypeKind("Enum", {}).forTypeKind("Scalar", {}).forTypeKind("Union", {}); - -interface ZodTypeEmitOptionsDeclarationProps - extends TypeEmitOptionsDeclarationProps {} From f7c3173691905e81b19f77fe7a1b2e9ed5792fd8 Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Thu, 15 Jan 2026 16:10:19 -0800 Subject: [PATCH 7/8] comments --- .../src/core/context/declaration-provider.ts | 4 ++++ .../src/core/declaration-provider.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/emitter-framework/src/core/context/declaration-provider.ts b/packages/emitter-framework/src/core/context/declaration-provider.ts index 6aa554aa88b..7c2c569dadf 100644 --- a/packages/emitter-framework/src/core/context/declaration-provider.ts +++ b/packages/emitter-framework/src/core/context/declaration-provider.ts @@ -3,6 +3,10 @@ import { type ComponentContext, createNamedContext, useContext } from "@alloy-js import type { Typekit } from "@typespec/compiler/typekit"; import { useTsp } from "./tsp-context.js"; +/** + * Provides the declaration provider that is used to get refkeys for + * declarations and determine if a type should be declared or referenced. + */ export const DeclarationProviderContext: ComponentContext = createNamedContext("DeclarationProviderContext"); diff --git a/packages/emitter-framework/src/core/declaration-provider.ts b/packages/emitter-framework/src/core/declaration-provider.ts index f70d96688d5..a341ec2ccf2 100644 --- a/packages/emitter-framework/src/core/declaration-provider.ts +++ b/packages/emitter-framework/src/core/declaration-provider.ts @@ -2,8 +2,20 @@ import { refkey, shallowReactive, type Refkey } from "@alloy-js/core"; import { getLocationContext, isVoidType, type MemberType, type Type } from "@typespec/compiler"; import type { Typekit } from "@typespec/compiler/typekit"; +/** + * This class tracks determines what types should be declared in the emit output + * and vends refkeys for them. The emitter framework will call `getRefkey` on + * the current declaration provider (see DeclarationProviderContext) to get + * refkeys for declarations it needs. Types passed to `getRefkey` and the refkey + * ultimately returned are added to the reactive `declarations` map. All types + * in this map should be emitted as declarations by the emitter. + */ export class DeclarationProvider { $: Typekit; + /** + * Reactive map of types to refkeys for all types which should be emitted + * as declarations. + */ declarations: Map = shallowReactive(new Map()); #staticMemberRefkeys: Map = new Map(); From 17ec28df20fb497c8f23e4e36a913c2b97f5ee6b Mon Sep 17 00:00:00 2001 From: Brian Terlson Date: Thu, 15 Jan 2026 16:12:08 -0800 Subject: [PATCH 8/8] remove garbage --- packages/http-client-js/test.tsp | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 packages/http-client-js/test.tsp diff --git a/packages/http-client-js/test.tsp b/packages/http-client-js/test.tsp deleted file mode 100644 index 07f91be1dfb..00000000000 --- a/packages/http-client-js/test.tsp +++ /dev/null @@ -1,9 +0,0 @@ -import "@typespec/http"; - -using Http; -@service(#{ title: "Widget Service" }) -namespace Test; - -model Widget is Record; - -op foo(): Widget;