diff --git a/.chronus/changes/tspd-improvements-2026-4-11-16-35-10-2.md b/.chronus/changes/tspd-improvements-2026-4-11-16-35-10-2.md
new file mode 100644
index 00000000000..24165af9323
--- /dev/null
+++ b/.chronus/changes/tspd-improvements-2026-4-11-16-35-10-2.md
@@ -0,0 +1,8 @@
+---
+# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
+changeKind: feature
+packages:
+ - "@typespec/tspd"
+---
+
+Improve render of complex emitter options
diff --git a/.chronus/changes/tspd-improvements-2026-4-11-16-35-10.md b/.chronus/changes/tspd-improvements-2026-4-11-16-35-10.md
new file mode 100644
index 00000000000..2826ee57115
--- /dev/null
+++ b/.chronus/changes/tspd-improvements-2026-4-11-16-35-10.md
@@ -0,0 +1,8 @@
+---
+# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
+changeKind: feature
+packages:
+ - "@typespec/tspd"
+---
+
+Render documentation for sub exports
diff --git a/packages/http-client-js/README.md b/packages/http-client-js/README.md
index 22fb437d909..9da489c22c3 100644
--- a/packages/http-client-js/README.md
+++ b/packages/http-client-js/README.md
@@ -46,4 +46,6 @@ See [Configuring output directory for more info](https://typespec.io/docs/handbo
**Type:** `string`
+**Default:** `"test-package"`
+
Name of the package as it will be in package.json
diff --git a/packages/http-server-csharp/README.md b/packages/http-server-csharp/README.md
index 3d6a8e83350..413512cd4b0 100644
--- a/packages/http-server-csharp/README.md
+++ b/packages/http-server-csharp/README.md
@@ -52,36 +52,48 @@ Skips formatting of generated C# Types. By default, C# files are formatted using
**Type:** `"models" | "all"`
+**Default:** `"all"`
+
Chooses which service artifacts to emit. choices include 'models' or 'all' artifacts.
### `emit-mocks`
**Type:** `"mocks-and-project-files" | "mocks-only" | "none"`
+**Default:** `"none"`
+
Emits mock implementations of business logic, setup code, and project files, enabling the service to respond to requests before a real implementation is provided
### `use-swaggerui`
**Type:** `boolean`
+**Default:** `false`
+
Configure a Swagger UI endpoint in the development configuration
### `openapi-path`
**Type:** `string`
+**Default:** `null`
+
Use openapi at the given path for generating SwaggerUI endpoints. By default, this will be 'openapi/openapi.yaml' if the 'use-swaggerui' option is enabled.
### `overwrite`
**Type:** `boolean`
+**Default:** `false`
+
When generating mock and project files, overwrite any existing files with the same name.
### `project-name`
**Type:** `string`
+**Default:** `"ServiceProject"`
+
The name of the generated project.
### `http-port`
@@ -100,4 +112,6 @@ The service https port when hosting the project locally.
**Type:** `"array" | "enumerable"`
+**Default:** `"array"`
+
Specifies the collection type to use: 'array' or 'enumerable'. The default is 'array'.
diff --git a/packages/http-server-js/README.md b/packages/http-server-js/README.md
index e05469a4147..b9b1ae3b87a 100644
--- a/packages/http-server-js/README.md
+++ b/packages/http-server-js/README.md
@@ -48,6 +48,8 @@ See [Configuring output directory for more info](https://typespec.io/docs/handbo
**Type:** `boolean`
+**Default:** `false`
+
If set to `true`, the emitter will generate a router that exposes an Express.js middleware function in addition to the ordinary Node.js HTTP server router.
If this option is not set to `true`, the `expressMiddleware` property will not be present on the generated router.
@@ -56,18 +58,24 @@ If this option is not set to `true`, the `expressMiddleware` property will not b
**Type:** `"temporal-polyfill" | "temporal" | "date-duration"`
+**Default:** `"temporal-polyfill"`
+
The type of datetime models to use for TypeSpecs DateTime and Duration types.
### `omit-unreachable-types`
**Type:** `boolean`
+**Default:** `false`
+
By default, the emitter will create interfaces that represent all models in the service namespace. If this option is set to `true`, the emitter will only emit those types that are reachable from an HTTP operation.
### `no-format`
**Type:** `boolean`
+**Default:** `false`
+
If set to `true`, the emitter will not format the generated code using Prettier.
## Functionality and generated code
diff --git a/packages/json-schema/README.md b/packages/json-schema/README.md
index b27ddf8e22c..1e3af685da6 100644
--- a/packages/json-schema/README.md
+++ b/packages/json-schema/README.md
@@ -97,6 +97,8 @@ When true, emit all references as json schema files, even if the referenced type
**Type:** `boolean`
+**Default:** `false`
+
If true, then for models emitted as object schemas we default `unevaluatedProperties` to `{ not: {} }`,
if not explicitly specified elsewhere.
Default: `false`
@@ -105,6 +107,8 @@ Default: `false`
**Type:** `"ignore" | "oneOf" | "anyOf"`
+**Default:** `"ignore"`
+
Strategy for emitting models with the @discriminator decorator:
- ignore: Emit as regular object schema (default). Derived models use allOf to reference their base model.
diff --git a/packages/openapi3/README.md b/packages/openapi3/README.md
index 6512a4c1a86..d0ec89cf3e1 100644
--- a/packages/openapi3/README.md
+++ b/packages/openapi3/README.md
@@ -44,10 +44,15 @@ See [Configuring output directory for more info](https://typespec.io/docs/handbo
### `file-type`
-**Type:** `string,array`
+**Type:** `"yaml" | "json" | ("yaml" | "json")[]`
If the content should be serialized as YAML or JSON. Can be a single value or an array to emit multiple formats. Default 'yaml', if not specified infer from the `output-file` extension
+**Options:**
+
+- `"yaml" | "json"`
+- `("yaml" | "json")[]`
+
### `output-file`
**Type:** `string`
@@ -86,12 +91,16 @@ Example Multiple service with versioning
### `openapi-versions`
-**Type:** `array`
+**Type:** `"3.0.0" | "3.1.0" | "3.2.0"`
+
+**Default:** `["3.0.0"]`
### `new-line`
**Type:** `"crlf" | "lf"`
+**Default:** `"lf"`
+
Set the newline character for emitting files.
### `omit-unreachable-types`
@@ -105,6 +114,8 @@ By default all types declared under the service namespace will be included. With
**Type:** `"inline-only" | "never"`
+**Default:** `"never"`
+
If the generated openapi types should have the `x-typespec-name` extension set with the name of the TypeSpec type that created it.
This extension is meant for debugging and should not be depended on.
@@ -112,6 +123,8 @@ This extension is meant for debugging and should not be depended on.
**Type:** `"double-int" | "int64"`
+**Default:** `"int64"`
+
How to handle safeint type. Options are:
- `double-int`: Will produce `type: integer, format: double-int`
@@ -123,6 +136,8 @@ Default: `int64`
**Type:** `boolean`
+**Default:** `false`
+
If true, then for models emitted as object schemas we default `additionalProperties` to false for
OpenAPI 3.0, and `unevaluatedProperties` to false for OpenAPI 3.1, if not explicitly specified elsewhere.
Default: `false`
@@ -138,7 +153,24 @@ See https://github.com/OAI/OpenAPI-Specification/discussions/4622 for discussion
### `operation-id-strategy`
-**Type:** `undefined`
+**Type:** `"parent-container" | "fqn" | "explicit-only" | object { kind, separator }`
+
+**Options:**
+
+- `"parent-container" | "fqn" | "explicit-only"` (default: `"parent-container"`)
+
+ Determines how to generate operation IDs when `@operationId` is not used.
+ Avaliable options are:
+
+- `parent-container`: Uses the parent namespace and operation name to generate the ID.
+- `fqn`: Uses the fully qualified name of the operation to generate the ID.
+- `explicit-only`: Only use explicitly defined operation IDs.
+- `object { kind, separator }`
+
+| Name | Type | Default | Description |
+| ----------- | ------------------------------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `kind` | `"parent-container" \| "fqn" \| "explicit-only"` | `"parent-container"` | Determines how to generate operation IDs when `@operationId` is not used.
Avaliable options are:
- `parent-container`: Uses the parent namespace and operation name to generate the ID.
- `fqn`: Uses the fully qualified name of the operation to generate the ID.
- `explicit-only`: Only use explicitly defined operation IDs. |
+| `separator` | `string` | | Separator used to join segment in the operation name. |
## Decorators
diff --git a/packages/tspd/src/ref-doc/emitters/markdown.ts b/packages/tspd/src/ref-doc/emitters/markdown.ts
index f6fb83f1a9f..4cf748c3497 100644
--- a/packages/tspd/src/ref-doc/emitters/markdown.ts
+++ b/packages/tspd/src/ref-doc/emitters/markdown.ts
@@ -11,6 +11,7 @@ import {
DecoratorRefDoc,
DeprecationNotice,
EmitterOptionRefDoc,
+ EmitterOptionVariantRefDoc,
EnumRefDoc,
ExampleRefDoc,
InterfaceRefDoc,
@@ -268,11 +269,22 @@ export class MarkdownRenderer {
content.push(this.templateParameters(union.templateParameters));
}
+ if (union.variants.size > 0) {
+ content.push(this.unionVariants(union));
+ }
+
content.push(this.examples(union.examples));
return this.typeSection(union, content);
}
+ unionVariants(union: UnionRefDoc): MarkdownDoc {
+ const rows = [...union.variants.values()].map((v) => {
+ return [v.name, this.ref(v.type.type), v.doc];
+ });
+ return section("Variants", [table([["Name", "Type", "Description"], ...rows])]);
+ }
+
scalar(scalar: ScalarRefDoc): MarkdownDoc {
const content: MarkdownDoc = ["", scalar.doc, codeblock(scalar.signature, "typespec"), ""];
@@ -401,17 +413,100 @@ export class MarkdownRenderer {
]),
);
for (const option of options) {
- content.push(
- section(`${inlinecode(option.name)}`, [
- `**Type:** ${inlinecode(option.type)}`,
- "",
- option.doc,
- ]),
- );
+ content.push(this.emitterOption(option));
}
return section("Emitter options", content);
}
+ emitterOptionTypeDisplay(option: { type: string; allowedValues?: readonly string[] }): string {
+ if (option.allowedValues && option.allowedValues.length > 0) {
+ return inlinecode(option.allowedValues.join(" | "));
+ }
+ return inlinecode(option.type);
+ }
+
+ emitterOption(option: EmitterOptionRefDoc): MarkdownDoc {
+ const content: MarkdownDoc = [];
+
+ // Deprecation notice
+ if (option.deprecated !== undefined) {
+ const message = option.deprecated || "This option is deprecated.";
+ content.push(this.deprecationNotice({ message }), "");
+ }
+
+ // Type line
+ content.push(`**Type:** ${this.emitterOptionTypeDisplay(option)}`);
+
+ // Default value
+ if (option.default !== undefined) {
+ content.push("", `**Default:** ${inlinecode(option.default)}`);
+ }
+
+ // Description
+ if (option.doc) {
+ content.push("", option.doc);
+ }
+
+ // Variants (oneOf)
+ if (option.variants && option.variants.length > 0) {
+ content.push("", "**Options:**", "");
+ for (const variant of option.variants) {
+ content.push(this.emitterOptionVariant(variant));
+ }
+ }
+
+ // Nested options
+ if (option.nestedOptions && option.nestedOptions.length > 0) {
+ content.push("", "**Properties:**", "");
+ const rows: string[][] = [["Name", "Type", "Default", "Description"]];
+ for (const nested of option.nestedOptions) {
+ rows.push([
+ inlinecode(nested.name),
+ this.emitterOptionTypeDisplay(nested),
+ nested.default !== undefined ? inlinecode(nested.default) : "",
+ nested.doc,
+ ]);
+ }
+ content.push(table(rows));
+ }
+
+ return section(`${inlinecode(option.name)}`, content);
+ }
+
+ emitterOptionVariant(variant: EmitterOptionVariantRefDoc): MarkdownDoc {
+ const content: MarkdownDoc = [];
+
+ // Variant type/values
+ const typeDisplay = this.emitterOptionTypeDisplay(variant);
+
+ let header = `- ${typeDisplay}`;
+ if (variant.default !== undefined) {
+ header += ` (default: ${inlinecode(variant.default)})`;
+ }
+ content.push(header);
+
+ if (variant.doc) {
+ content.push("", ` ${variant.doc}`);
+ }
+
+ // Nested options for object variants
+ if (variant.nestedOptions && variant.nestedOptions.length > 0) {
+ content.push("");
+ const rows: string[][] = [["Name", "Type", "Default", "Description"]];
+ for (const nested of variant.nestedOptions) {
+ rows.push([
+ inlinecode(nested.name),
+ this.emitterOptionTypeDisplay(nested),
+ nested.default !== undefined ? inlinecode(nested.default) : "",
+ nested.doc,
+ ]);
+ }
+ content.push(" ", table(rows));
+ }
+
+ return content;
+ }
+
linterUsage(refDoc: TypeSpecRefDoc) {
if (refDoc.linter === undefined) {
return [];
diff --git a/packages/tspd/src/ref-doc/emitters/starlight.ts b/packages/tspd/src/ref-doc/emitters/starlight.ts
index dbbec06888d..f43142108ea 100644
--- a/packages/tspd/src/ref-doc/emitters/starlight.ts
+++ b/packages/tspd/src/ref-doc/emitters/starlight.ts
@@ -3,6 +3,7 @@ import {
LinterRuleRefDoc,
NamedTypeRefDoc,
RefDocEntity,
+ SubExportRefDoc,
TypeSpecLibraryRefDoc,
TypeSpecRefDoc,
} from "../types.js";
@@ -56,6 +57,16 @@ export function renderToAstroStarlightMarkdown(
files["linter.md"] = linter;
}
+ // Render sub-exports
+ if (refDoc.subExports) {
+ for (const [exportPath, subExport] of refDoc.subExports) {
+ const subFiles = renderSubExport(renderer, refDoc, exportPath, subExport, options);
+ for (const [name, content] of Object.entries(subFiles)) {
+ files[name] = content;
+ }
+ }
+ }
+
return files;
}
@@ -270,6 +281,112 @@ function renderLinter(
return renderMarkdowDoc(content, 2);
}
+function renderSubExport(
+ renderer: StarlightRenderer,
+ refDoc: TypeSpecRefDoc,
+ exportPath: string,
+ subExport: SubExportRefDoc,
+ options: RenderToStarlightMarkdownOptions,
+): Record {
+ const files: Record = {};
+ // Use the export path as a directory prefix (e.g., "./streams" -> "streams/")
+ const dirPrefix = exportPath.replace(/^\.\//, "") + "/";
+ const displayName = exportPath.replace(/^\.\//, "");
+
+ // Decorators
+ if (subExport.namespaces.some((x) => x.decorators.length > 0)) {
+ const content: MarkdownDoc = [
+ "---",
+ `title: "Decorators (${displayName})"`,
+ `description: "Decorators exported by ${refDoc.name}/${displayName}"`,
+ "toc_min_heading_level: 2",
+ "toc_max_heading_level: 3",
+ ];
+ if (options.llmstxt) {
+ content.push("llmstxt: true");
+ }
+ content.push("---");
+ content.push(renderer.decoratorsSection(subExport));
+ files[`${dirPrefix}decorators.md`] = renderMarkdowDoc(content, 2);
+ }
+
+ // Interfaces and Operations
+ if (subExport.namespaces.some((x) => x.operations.length > 0 || x.interfaces.length > 0)) {
+ const content: MarkdownDoc = [
+ "---",
+ `title: "Interfaces and Operations (${displayName})"`,
+ `description: "Interfaces and Operations exported by ${refDoc.name}/${displayName}"`,
+ ];
+ if (options.llmstxt) {
+ content.push("llmstxt: true");
+ }
+ content.push("---");
+ content.push(
+ groupByNamespace(subExport.namespaces, (namespace) => {
+ if (namespace.operations.length === 0 && namespace.interfaces.length === 0) {
+ return undefined;
+ }
+ const nsContent: MarkdownDoc = [];
+ for (const iface of namespace.interfaces) {
+ nsContent.push(renderer.interface(iface), "");
+ }
+ for (const operation of namespace.operations) {
+ nsContent.push(renderer.operation(operation), "");
+ }
+ return nsContent;
+ }),
+ );
+ files[`${dirPrefix}interfaces.md`] = renderMarkdowDoc(content, 2);
+ }
+
+ // Data types (models, enums, unions, scalars)
+ if (
+ subExport.namespaces.some(
+ (x) =>
+ x.models.length > 0 || x.enums.length > 0 || x.unions.length > 0 || x.scalars.length > 0,
+ )
+ ) {
+ const content: MarkdownDoc = [
+ "---",
+ `title: "Data types (${displayName})"`,
+ `description: "Data types exported by ${refDoc.name}/${displayName}"`,
+ ];
+ if (options.llmstxt) {
+ content.push("llmstxt: true");
+ }
+ content.push("---");
+ content.push(
+ groupByNamespace(subExport.namespaces, (namespace) => {
+ const modelCount =
+ namespace.models.length +
+ namespace.enums.length +
+ namespace.unions.length +
+ namespace.scalars.length;
+ if (modelCount === 0) {
+ return undefined;
+ }
+ const nsContent: MarkdownDoc = [];
+ for (const model of namespace.models) {
+ nsContent.push(renderer.model(model), "");
+ }
+ for (const e of namespace.enums) {
+ nsContent.push(renderer.enum(e), "");
+ }
+ for (const union of namespace.unions) {
+ nsContent.push(renderer.union(union), "");
+ }
+ for (const scalar of namespace.scalars) {
+ nsContent.push(renderer.scalar(scalar), "");
+ }
+ return nsContent;
+ }),
+ );
+ files[`${dirPrefix}data-types.md`] = renderMarkdowDoc(content, 2);
+ }
+
+ return files;
+}
+
export class StarlightRenderer extends MarkdownRenderer {
headingTitle(item: NamedTypeRefDoc): string {
// Set an explicit anchor id.
diff --git a/packages/tspd/src/ref-doc/experimental.ts b/packages/tspd/src/ref-doc/experimental.ts
index a7cd57538bc..188dd252b3a 100644
--- a/packages/tspd/src/ref-doc/experimental.ts
+++ b/packages/tspd/src/ref-doc/experimental.ts
@@ -35,8 +35,14 @@ export async function generateLibraryDocs(
await mkdir(outputDir, { recursive: true });
const config = await prettier.resolveConfig(libraryPath);
for (const [name, content] of Object.entries(files)) {
+ const filePath = joinPaths(outputDir, name);
+ // Ensure parent directory exists for sub-export files
+ const dir = filePath.substring(0, filePath.lastIndexOf("/"));
+ if (dir !== outputDir) {
+ await mkdir(dir, { recursive: true });
+ }
const formatted = await formatMarkdown(name, content, config);
- await writeFile(joinPaths(outputDir, name), formatted);
+ await writeFile(filePath, formatted);
}
const readme = await formatMarkdown(
joinPaths(libraryPath, "README.md"),
diff --git a/packages/tspd/src/ref-doc/extractor.ts b/packages/tspd/src/ref-doc/extractor.ts
index 98f0e0529cf..55614f5d95a 100644
--- a/packages/tspd/src/ref-doc/extractor.ts
+++ b/packages/tspd/src/ref-doc/extractor.ts
@@ -36,6 +36,7 @@ import {
Type,
TypeSpecLibrary,
Union,
+ UnionVariant,
type PackageJson,
} from "@typespec/compiler";
import { SyntaxKind, type DocUnknownTagNode } from "@typespec/compiler/ast";
@@ -46,6 +47,7 @@ import {
DecoratorRefDoc,
DeprecationNotice,
EmitterOptionRefDoc,
+ EmitterOptionVariantRefDoc,
EnumMemberRefDoc,
EnumRefDoc,
ExampleRefDoc,
@@ -61,9 +63,11 @@ import {
RefDocEntity,
ReferencableElement,
ScalarRefDoc,
+ SubExportRefDoc,
TypeSpecLibraryRefDoc,
TypeSpecRefDocBase,
UnionRefDoc,
+ UnionVariantRefDoc,
} from "./types.js";
import { getQualifier, getTypeSignature } from "./utils/type-signature.js";
@@ -96,11 +100,13 @@ export async function extractLibraryRefDocs(
getNamedTypeRefDoc: (type) => undefined,
};
const tspMain = getExport(pkgJson, ".", "typespec");
+ let mainSourceFiles: Set | undefined;
if (tspMain) {
const main = resolvePath(libraryPath, tspMain);
const program = await compile(NodeHost, main, {
parseOptions: { comments: true, docs: true },
});
+ mainSourceFiles = new Set(program.sourceFiles.keys());
const tspEmitter = diagnostics.pipe(extractRefDocs(program));
Object.assign(refDoc, tspEmitter);
for (const diag of program.diagnostics ?? []) {
@@ -123,6 +129,12 @@ export async function extractLibraryRefDocs(
}
}
+ // Extract sub-exports
+ const subExports = await extractSubExports(libraryPath, pkgJson, diagnostics, mainSourceFiles);
+ if (subExports.size > 0) {
+ refDoc.subExports = subExports;
+ }
+
return diagnostics.wrap(refDoc);
}
@@ -131,11 +143,66 @@ async function readPackageJson(libraryPath: string): Promise {
return JSON.parse(buffer.toString());
}
+async function extractSubExports(
+ libraryPath: string,
+ pkgJson: PackageJson,
+ diagnostics: { pipe: (result: [T, readonly Diagnostic[]]) => T; add: (d: Diagnostic) => void },
+ mainSourceFiles?: Set,
+): Promise