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
8 changes: 8 additions & 0 deletions .chronus/changes/graphql-regen-auto-accessor-docs-2026-9-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: internal
packages:
- "@typespec/graphql"
---

Regenerate the decorator signatures to pick up the doc comments now emitted for auto decorator
accessors.
15 changes: 15 additions & 0 deletions .chronus/changes/tspd-auto-accessor-docs-2026-9-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
changeKind: fix
packages:
- "@typespec/tspd"
---

Document the generated auto decorator accessors with the description of the decorator they read or
write, so libraries re-exporting them satisfy api-extractor's `ae-undocumented` rule.

```ts
/** Mark a model as a GraphQL input type in the emitted schema. */
export function isInputType(program: Program, target: Model): boolean {
return hasAutoDecorator(program, "TypeSpec.GraphQL.inputType", target);
}
```
9 changes: 9 additions & 0 deletions .chronus/changes/tspd-honor-library-config-2026-9-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
changeKind: fix
packages:
- "@typespec/tspd"
---

Honor the library's own `tspconfig.yaml` when generating signatures and reference documentation, so
libraries that opt into a compiler feature (such as `auto-decorators`) no longer report errors during
`gen-extern-signature` and `doc`.
52 changes: 52 additions & 0 deletions packages/graphql/generated-defs/TypeSpec.GraphQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,40 +184,92 @@ export type TypeSpecGraphQLDecorators = {
specifiedBy: SpecifiedByDecorator;
};

/**
* Mark a model as a GraphQL input type in the emitted schema.
*
* This decorator is applied automatically by the mutation engine when it produces
* a model that is used in input position. The emitter uses this to emit the model
* as an `input` type rather than an object `type`.
*/
export function isInputType(program: Program, target: Model): boolean {
return hasAutoDecorator(program, "TypeSpec.GraphQL.inputType", target);
}

/**
* Mark a model as a GraphQL input type in the emitted schema.
*
* This decorator is applied automatically by the mutation engine when it produces
* a model that is used in input position. The emitter uses this to emit the model
* as an `input` type rather than an object `type`.
*/
export function setInputType(program: Program, target: Model): void {
setAutoDecorator(program, "TypeSpec.GraphQL.inputType", target);
}

/**
* Mark a field, operation, or type as nullable in the emitted GraphQL schema.
*
* Applied automatically by the mutation engine when it strips `| null` from
* union types, and can also be applied directly in TypeSpec source.
*/
export function isNullable(
program: Program,
target: ModelProperty | Operation | Union | Model,
): boolean {
return hasAutoDecorator(program, "TypeSpec.GraphQL.nullable", target);
}

/**
* Mark a field, operation, or type as nullable in the emitted GraphQL schema.
*
* Applied automatically by the mutation engine when it strips `| null` from
* union types, and can also be applied directly in TypeSpec source.
*/
export function setNullable(
program: Program,
target: ModelProperty | Operation | Union | Model,
): void {
setAutoDecorator(program, "TypeSpec.GraphQL.nullable", target);
}

/**
* Mark a field or operation as having nullable array elements in the emitted GraphQL schema.
*
* Applied automatically by the mutation engine when it detects `Array<T | null>`
* patterns. Causes the emitter to emit `[T]` instead of `[T!]`.
*/
export function isNullableElements(program: Program, target: ModelProperty | Operation): boolean {
return hasAutoDecorator(program, "TypeSpec.GraphQL.nullableElements", target);
}

/**
* Mark a field or operation as having nullable array elements in the emitted GraphQL schema.
*
* Applied automatically by the mutation engine when it detects `Array<T | null>`
* patterns. Causes the emitter to emit `[T]` instead of `[T!]`.
*/
export function setNullableElements(program: Program, target: ModelProperty | Operation): void {
setAutoDecorator(program, "TypeSpec.GraphQL.nullableElements", target);
}

/**
* Mark a model as a `@oneOf` input object in the emitted GraphQL schema.
*
* This decorator is applied automatically by the mutation engine when it converts
* a union type in input context to a synthetic input object (since GraphQL unions
* are output-only). The emitter uses this to emit the `@oneOf` directive.
*/
export function isOneOf(program: Program, target: Model): boolean {
return hasAutoDecorator(program, "TypeSpec.GraphQL.oneOf", target);
}

/**
* Mark a model as a `@oneOf` input object in the emitted GraphQL schema.
*
* This decorator is applied automatically by the mutation engine when it converts
* a union type in input context to a synthetic input object (since GraphQL unions
* are output-only). The emitter uses this to emit the `@oneOf` directive.
*/
export function setOneOf(program: Program, target: Model): void {
setAutoDecorator(program, "TypeSpec.GraphQL.oneOf", target);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { code, For, List } from "@alloy-js/core";
import * as ts from "@alloy-js/typescript";
import type { Decorator } from "@typespec/compiler";
import { typespecCompiler } from "../external-packages/compiler.js";
import type { DecoratorSignature } from "../types.js";
import { ParameterTsType, TargetParameterTsType } from "./decorator-signature-type.js";
Expand Down Expand Up @@ -51,17 +52,20 @@ function AutoDecoratorReader(props: Readonly<AutoDecoratorAccessorProps>) {
if (params.length === 0) {
// No-arg auto decorator — generate `is*` function
return (
<ts.FunctionDeclaration
export
name={`is${capitalizedName}`}
parameters={[
{ name: "program", type: typespecCompiler.Program },
{ name: decorator.target.name, type: targetType },
]}
returnType="boolean"
>
{code`return ${typespecCompiler.hasAutoDecorator}(program, "${fqn}", ${decorator.target.name});`}
</ts.FunctionDeclaration>
<List hardline>
<AccessorDoc decorator={decorator} />
<ts.FunctionDeclaration
export
name={`is${capitalizedName}`}
parameters={[
{ name: "program", type: typespecCompiler.Program },
{ name: decorator.target.name, type: targetType },
]}
returnType="boolean"
>
{code`return ${typespecCompiler.hasAutoDecorator}(program, "${fqn}", ${decorator.target.name});`}
</ts.FunctionDeclaration>
</List>
);
}

Expand Down Expand Up @@ -99,17 +103,20 @@ function AutoDecoratorReader(props: Readonly<AutoDecoratorAccessorProps>) {
}

return (
<ts.FunctionDeclaration
export
name={`get${capitalizedName}`}
parameters={[
{ name: "program", type: typespecCompiler.Program },
{ name: decorator.target.name, type: targetType },
]}
returnType={returnType}
>
{body}
</ts.FunctionDeclaration>
<List hardline>
<AccessorDoc decorator={decorator} />
<ts.FunctionDeclaration
export
name={`get${capitalizedName}`}
parameters={[
{ name: "program", type: typespecCompiler.Program },
{ name: decorator.target.name, type: targetType },
]}
returnType={returnType}
>
{body}
</ts.FunctionDeclaration>
</List>
);
}

Expand Down Expand Up @@ -161,13 +168,60 @@ function AutoDecoratorSetter(props: Readonly<AutoDecoratorAccessorProps>) {
}

return (
<ts.FunctionDeclaration
export
name={`set${capitalizedName}`}
parameters={parameters}
returnType="void"
>
{body}
</ts.FunctionDeclaration>
<List hardline>
<AccessorDoc decorator={decorator} />
<ts.FunctionDeclaration
export
name={`set${capitalizedName}`}
parameters={parameters}
returnType="void"
>
{body}
</ts.FunctionDeclaration>
</List>
);
}

/**
* Render the decorator's own documentation as the accessor doc comment.
*
* Only the description is carried over: the `@param` tags of the decorator describe its TypeSpec
* parameters, which do not line up with the accessor signatures.
*
* The comment is rendered standalone rather than through the `doc` prop of
* `ts.FunctionDeclaration`, because that also emits `@param {Type}` tags whose type references count
* as value usages and would turn the type-only imports of this file into value imports.
*/
function AccessorDoc(props: Readonly<{ decorator: Decorator }>) {
const description = getDocDescription(props.decorator);
if (description === undefined) {
return null;
}
const lines = description.split("\n");
const comment =
lines.length === 1
? `/** ${lines[0]} */`
: [`/**`, ...lines.map((line) => ` * ${line}`.trimEnd()), ` */`].join("\n");
return <>{comment}</>;
}

/** Get the description of a decorator, excluding any doc tag. */
function getDocDescription(decorator: Decorator): string | undefined {
const docs = decorator.node?.docs;
if (docs === undefined || docs.length === 0) {
return undefined;
}

const lines: string[] = [];
for (const doc of docs) {
for (const content of doc.content) {
for (const line of content.text.split("\n")) {
// Issue to escape @internal and other tsdoc tags https://github.com/microsoft/TypeScript/issues/47679
lines.push(line.replaceAll("@internal", "@_internal"));
}
}
}

const description = lines.join("\n").trim();
return description === "" ? undefined : description;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from "@typespec/compiler";
import prettier from "prettier";
import { createDiagnostic } from "../ref-doc/lib.js";
import { resolveLibraryCompilerOptions } from "../utils/library-config.js";
import { generateSignatures } from "./components/entity-signatures.js";
import type { DecoratorSignature, EntitySignature, FunctionSignature } from "./types.js";

Expand Down Expand Up @@ -155,6 +156,7 @@ export async function generateExternSignatureForExports(
for (const entry of exports) {
programs.push(
await compile(host, entry.typespecEntrypoint, {
...(await resolveLibraryCompilerOptions(host, entry.typespecEntrypoint)),
parseOptions: { comments: true, docs: true },
}),
);
Expand Down
2 changes: 2 additions & 0 deletions packages/tspd/src/ref-doc/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Diagnostic } from "@typespec/compiler";
import { compile, createDiagnosticCollector, joinPaths, NodeHost } from "@typespec/compiler";
import { mkdir, writeFile } from "fs/promises";
import prettier from "prettier";
import { resolveLibraryCompilerOptions } from "../utils/library-config.js";
import { generateJsApiDocs } from "./api-docs.js";
import { renderReadme } from "./emitters/markdown.js";
import { renderToAstroStarlightMarkdown } from "./emitters/starlight.js";
Expand Down Expand Up @@ -74,6 +75,7 @@ export async function resolveLibraryRefDocsBase(
if (pkgJson.tspMain) {
const main = joinPaths(libraryPath, pkgJson.tspMain);
const program = await compile(NodeHost, main, {
...(await resolveLibraryCompilerOptions(NodeHost, main)),
parseOptions: { comments: true, docs: true },
});
const refDoc = diagnostics.pipe(extractRefDocs(program, options));
Expand Down
3 changes: 3 additions & 0 deletions packages/tspd/src/ref-doc/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
import { SyntaxKind, type DocUnknownTagNode } from "@typespec/compiler/ast";
import { readFile } from "fs/promises";
import { pathToFileURL } from "url";
import { resolveLibraryCompilerOptions } from "../utils/library-config.js";
import { createDiagnostic, reportDiagnostic } from "./lib.js";
import type {
DecoratorRefDoc,
Expand Down Expand Up @@ -109,6 +110,7 @@ export async function extractLibraryRefDocs(
if (tspMain) {
const main = resolvePath(libraryPath, tspMain);
const program = await compile(NodeHost, main, {
...(await resolveLibraryCompilerOptions(NodeHost, main)),
parseOptions: { comments: true, docs: true },
});
mainSourceFiles = new Set(program.sourceFiles.keys());
Expand Down Expand Up @@ -203,6 +205,7 @@ async function extractSubExports(
const main = resolvePath(libraryPath, tspEntry);
try {
const program = await compile(NodeHost, main, {
...(await resolveLibraryCompilerOptions(NodeHost, main)),
parseOptions: { comments: true, docs: true },
});
const subRefDoc = diagnostics.pipe(
Expand Down
19 changes: 19 additions & 0 deletions packages/tspd/src/utils/library-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
resolveCompilerOptions,
type CompilerHost,
type CompilerOptions,
} from "@typespec/compiler";

/**
* Resolve the compiler options from the library's own `tspconfig.yaml`, so that features it opts
* into (such as `auto-decorators`) apply when tspd compiles it.
*
* tspd only ever inspects a library, so emitting is always disabled.
*/
export async function resolveLibraryCompilerOptions(
host: CompilerHost,
entrypoint: string,
): Promise<CompilerOptions> {
const [options] = await resolveCompilerOptions(host, { cwd: process.cwd(), entrypoint });
return { ...options, noEmit: true };
}
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,49 @@ export function setMyMeta(program: Program, target: Model, value: { name: string
});
});

it("documents accessors with the decorator description, dropping doc tags", async () => {
await expectSignatures({
code: `
/**
* Specify the minimum number of instances the array must contain.
*
* @param value The minimum number of instances.
*/
auto dec myMin(target: Model, value: valueof int32);
`,
expected: `
import { getAutoDecoratorValue, type Model, type Program, setAutoDecorator } from "@typespec/compiler";

/** Specify the minimum number of instances the array must contain. */
export function getMyMin(program: Program, target: Model): number | undefined {
return getAutoDecoratorValue(program, "myMin", target)?.["value"] as any;
}

/** Specify the minimum number of instances the array must contain. */
export function setMyMin(program: Program, target: Model, value: number): void {
setAutoDecorator(program, "myMin", target, { value: value });
}
`,
});
});

it("renders a multi line description as a block comment", async () => {
const result = await generateDecoratorSignatures(`
/**
* First line.
*
* Second line.
*/
auto dec myFlag(target: Model);
`);
expect(result).toContain(`/**
* First line.
*
* Second line.
*/
export function isMyFlag`);
});

it("generates accessor with fully-qualified name for namespaced auto decorator", async () => {
const [{ program }] = await Tester.compileAndDiagnose(
`
Expand Down
Loading