Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .chronus/changes/ef-csharp-authorable-declarations-2026-8-5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
changeKind: feature
packages:
- "@typespec/emitter-framework"
---

Let emitters author the C# declaration components instead of forking them

- `ClassDeclaration` accepts an explicit `properties` list and extra members as `children`.
- `Property` accepts every Alloy property prop, plus `name` and `csharpType` overrides.
- `EnumDeclaration` accepts an explicit `members` list and a `jsonAttributes` prop.
- `JsonConverter` accepts `doc`, access modifiers, extra members, an explicit `csharpType`, and a `readReturns` override.

```tsx
<ClassDeclaration type={model} properties={model.properties.values().filter(isVisible)} partial>
<Constructor />
</ClassDeclaration>
```
9 changes: 9 additions & 0 deletions .chronus/changes/ef-csharp-type-expression-total-2026-8-5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
changeKind: fix
packages:
- "@typespec/emitter-framework"
---

Make the C# `TypeExpression` handle every type kind instead of throwing

`Tuple`, `StringTemplate`, `EnumMember`, `ModelProperty`, `UnionVariant`, template parameters and the full `Intrinsic` set are now supported, and an unsupported type reports a diagnostic and falls back to `object` rather than throwing. Also fixes the C# components reporting a TypeScript diagnostic for unsupported scalars, and corrects the C# expressions for the `null` and `never` intrinsics.
20 changes: 20 additions & 0 deletions .chronus/changes/ef-declaration-overrides-2026-8-5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
changeKind: feature
packages:
- "@typespec/emitter-framework"
---

Support declaration overrides in `Experimental_ComponentOverrides`

Only `reference` overrides were dispatched, so an emitter could customize how a type is referenced but not how it is declared, forcing it to fork the framework's declaration components. The C# `ClassDeclaration`, `Property` and `EnumDeclaration` now render through the override point.

```tsx
const overrides = Experimental_ComponentOverridesConfig().forTypeKind("ModelProperty", {
declaration: (props) =>
props.type.name === "id" ? (
<props.Declaration {...props.declarationProps} name="Identifier" />
) : (
props.default
),
});
```
Original file line number Diff line number Diff line change
Expand Up @@ -51,26 +51,54 @@ export interface Experimental_OverrideReferenceProps<
member?: ModelProperty;
}

/**
* Fallback props type for declaration overrides.
*
* Declaration props are language specific (`cs.ClassDeclarationProps`, `ts.VarDeclarationProps`,
* ...) and cannot be derived from the TypeSpec type, so they default to a permissive record.
* Pass the concrete props type explicitly to
* {@link Experimental_ComponentOverridesClass.forType} /
* {@link Experimental_ComponentOverridesClass.forTypeKind} to get full type checking.
*/
export type Experimental_DefaultDeclarationProps = Record<string, any>;

export interface Experimental_OverrideDeclareProps<
TCustomType extends Type,
TDeclarationProps = Experimental_DefaultDeclarationProps,
> extends Experimental_OverrideEmitPropsBase<TCustomType> {
Declaration: ComponentDefinition<Experimental_CustomTypeToProps<TCustomType>>;
declarationProps: Experimental_CustomTypeToProps<TCustomType>;
/**
* The component that produces the default declaration. Call it with (a modified copy of)
* {@link declarationProps} to reuse the framework's rendering.
*/
Declaration: ComponentDefinition<TDeclarationProps>;
/** The props the framework would have used to render the declaration. */
declarationProps: TDeclarationProps;
}

export type Experimental_OverrideDeclarationComponent<TCustomType extends Type> =
ComponentDefinition<Experimental_OverrideDeclareProps<TCustomType>>;
export type Experimental_OverrideDeclarationComponent<
TCustomType extends Type,
TDeclarationProps = Experimental_DefaultDeclarationProps,
> = ComponentDefinition<Experimental_OverrideDeclareProps<TCustomType, TDeclarationProps>>;

export type Experimental_OverrideReferenceComponent<TCustomType extends Type> = ComponentDefinition<
Experimental_OverrideReferenceProps<TCustomType>
>;

export interface Experimental_ComponentOverridesConfigBase<TCustomType extends Type> {
export interface Experimental_ComponentOverridesConfigBase<
TCustomType extends Type,
TDeclarationProps = Experimental_DefaultDeclarationProps,
> {
/**
* Override when this type is referenced.
* e.g. When used in <TypeExpression type={type} />
*/
reference?: Experimental_OverrideReferenceComponent<TCustomType>;

/**
* Override when this type is declared.
* e.g. When used in <ClassDeclaration type={type} />
*/
declaration?: Experimental_OverrideDeclarationComponent<TCustomType, TDeclarationProps>;
}

export interface Experimental_ComponentOverridesProps {
Expand Down Expand Up @@ -112,11 +140,32 @@ export interface Experimental_OverridableComponentReferenceProps<
member?: ModelProperty;
}

export type Experimental_OverridableComponentProps<T extends Type> =
Experimental_OverridableComponentReferenceProps<T>;
export interface Experimental_OverridableComponentDeclarationProps<
T extends Type,
TDeclarationProps,
> extends Experimental_OverrideTypeComponentCommonProps<T> {
/**
* Pass when rendering a declaration of the provided type or type kind.
*/
declaration: true;

/**
* The component that produces the default declaration.
*/
Declaration: ComponentDefinition<TDeclarationProps>;

/**
* The props the framework would have used to render the declaration.
*/
declarationProps: TDeclarationProps;
}

export type Experimental_OverridableComponentProps<T extends Type, TDeclarationProps = unknown> =
| Experimental_OverridableComponentReferenceProps<T>
| Experimental_OverridableComponentDeclarationProps<T, TDeclarationProps>;

export function Experimental_OverridableComponent<T extends Type>(
props: Experimental_OverridableComponentProps<T>,
export function Experimental_OverridableComponent<T extends Type, TDeclarationProps = unknown>(
props: Experimental_OverridableComponentProps<T, TDeclarationProps>,
) {
const options = useOverrides();
const { $ } = useTsp();
Expand All @@ -133,5 +182,17 @@ export function Experimental_OverridableComponent<T extends Type>(
return <CustomComponent type={props.type} member={props.member} default={props.children} />;
}

if ("declaration" in props && props.declaration && descriptor.declaration) {
const CustomComponent = descriptor.declaration;
return (
<CustomComponent
type={props.type}
default={props.children}
Declaration={props.Declaration}
declarationProps={props.declarationProps}
/>
);
}

return <>{props.children}</>;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Program, Scalar, Type } from "@typespec/compiler";
import { $ } from "@typespec/compiler/typekit";
import type { Experimental_ComponentOverridesConfigBase } from "./component-overrides.jsx";
import type {
Experimental_ComponentOverridesConfigBase,
Experimental_DefaultDeclarationProps,
} from "./component-overrides.jsx";

const getOverrideForTypeSym: unique symbol = Symbol.for("ef-ts:getOverrideForType");
const getOverrideForTypeKindSym: unique symbol = Symbol.for("ef-ts:getOverrideForTypeKind");
Expand All @@ -14,19 +17,28 @@ export const Experimental_ComponentOverridesConfig = function () {
};

export class Experimental_ComponentOverridesClass {
#typeEmitOptions: Map<Type, Experimental_ComponentOverridesConfigBase<any>> = new Map();
#typeKindEmitOptions: Map<Type["kind"], Experimental_ComponentOverridesConfigBase<any>> =
#typeEmitOptions: Map<Type, Experimental_ComponentOverridesConfigBase<any, any>> = new Map();
#typeKindEmitOptions: Map<Type["kind"], Experimental_ComponentOverridesConfigBase<any, any>> =
new Map();

forType<const T extends Type>(type: T, options: Experimental_ComponentOverridesConfigBase<T>) {
forType<const T extends Type, TDeclarationProps = Experimental_DefaultDeclarationProps>(
type: T,
options: Experimental_ComponentOverridesConfigBase<T, TDeclarationProps>,
) {
this.#typeEmitOptions.set(type, options);

return this;
}

forTypeKind<const TKind extends Type["kind"]>(
forTypeKind<
const TKind extends Type["kind"],
TDeclarationProps = Experimental_DefaultDeclarationProps,
>(
typeKind: TKind,
options: Experimental_ComponentOverridesConfigBase<Extract<Type, { kind: TKind }>>,
options: Experimental_ComponentOverridesConfigBase<
Extract<Type, { kind: TKind }>,
TDeclarationProps
>,
) {
this.#typeKindEmitOptions.set(typeKind, options);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,119 @@ describe("with doc comments", () => {
`);
});
});

describe("declaration overrides", () => {
it("replaces a class declaration entirely", async () => {
const { TestModel } = await runner.compile(t.code`
model ${t.model("TestModel")} {
Prop1: string;
}
`);

const overrides = Experimental_ComponentOverridesConfig().forTypeKind("Model", {
declaration: () => "class Replaced {}",
});

expect(
<Wrapper>
<Experimental_ComponentOverrides overrides={overrides}>
<ClassDeclaration type={TestModel} />
</Experimental_ComponentOverrides>
</Wrapper>,
).toRenderTo(`class Replaced {}`);
});

it("re-renders the default declaration with modified props", async () => {
const { TestModel } = await runner.compile(t.code`
model ${t.model("TestModel")} {}
`);

const overrides = Experimental_ComponentOverridesConfig().forTypeKind("Model", {
declaration: (props) => (
<props.Declaration {...props.declarationProps} name="Renamed" partial />
),
});

expect(
<Wrapper>
<Experimental_ComponentOverrides overrides={overrides}>
<ClassDeclaration type={TestModel} />
</Experimental_ComponentOverrides>
</Wrapper>,
).toRenderTo(`partial class Renamed {}`);
});

it("falls back to the default when only a reference override is configured", async () => {
const { TestModel } = await runner.compile(t.code`
model ${t.model("TestModel")} {}
`);

const overrides = Experimental_ComponentOverridesConfig().forTypeKind("Model", {
reference: () => "Nope",
});

expect(
<Wrapper>
<Experimental_ComponentOverrides overrides={overrides}>
<ClassDeclaration type={TestModel} />
</Experimental_ComponentOverrides>
</Wrapper>,
).toRenderTo(`class TestModel {}`);
});

it("overrides a property declaration", async () => {
const { TestModel } = await runner.compile(t.code`
model ${t.model("TestModel")} {
Prop1: string;
Prop2: int32;
}
`);

const overrides = Experimental_ComponentOverridesConfig().forTypeKind("ModelProperty", {
declaration: (props) =>
props.type.name === "Prop1" ? "public string Custom { get; }" : props.default,
});

expect(
<Wrapper>
<Experimental_ComponentOverrides overrides={overrides}>
<ClassDeclaration type={TestModel} />
</Experimental_ComponentOverrides>
</Wrapper>,
).toRenderTo(d`
class TestModel
{
public string Custom { get; }

public required int Prop2 { get; set; }
}
`);
});

it("overrides an enum declaration", async () => {
const { TestEnum } = await runner.compile(t.code`
enum ${t.enum("TestEnum")} {
A,
B,
}
`);

const overrides = Experimental_ComponentOverridesConfig().forTypeKind("Enum", {
declaration: (props) => <props.Declaration {...props.declarationProps} name="RenamedEnum" />,
});

expect(
<Wrapper>
<Experimental_ComponentOverrides overrides={overrides}>
<EnumDeclaration type={TestEnum} />
</Experimental_ComponentOverrides>
</Wrapper>,
).toRenderTo(d`
enum RenamedEnum
{
A,
B
}
`);
});
});
Loading
Loading