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> { + const subExports = new Map(); + const exports = (pkgJson as any).exports; + if (!exports || typeof exports !== "object") { + return subExports; + } + + for (const [exportPath, exportValue] of Object.entries(exports)) { + // Skip the main export — already handled + if (exportPath === ".") continue; + + // Only process exports that have a typespec condition + const tspEntry = exportValue?.typespec; + if (!tspEntry) continue; + + const main = resolvePath(libraryPath, tspEntry); + try { + const program = await compile(NodeHost, main, { + parseOptions: { comments: true, docs: true }, + }); + const subRefDoc = diagnostics.pipe( + extractRefDocs(program, { + sourceFilter: mainSourceFiles + ? (sourcePath) => !mainSourceFiles.has(sourcePath) + : undefined, + }), + ); + // Only include if it actually has content + if (subRefDoc.namespaces.length > 0) { + subExports.set(exportPath, { + path: exportPath, + ...subRefDoc, + }); + } + for (const diag of program.diagnostics ?? []) { + diagnostics.add(diag); + } + } catch { + // Skip sub-exports that fail to compile + } + } + + return subExports; +} + export interface ExtractRefDocOptions { namespaces?: { include?: string[]; exclude?: string[]; }; + /** + * Filter to restrict which types are included based on their source file path. + * When provided, only types declared in files for which this returns true will be included. + */ + sourceFilter?: (sourcePath: string) => boolean; } function resolveNamespaces( @@ -159,6 +226,12 @@ function resolveNamespaces( if (namespace.name === "Private") { return; } + if (options.sourceFilter && namespace.node) { + const loc = getSourceLocation(namespace.node); + if (!loc.isSynthetic && !options.sourceFilter(loc.file.path)) { + return; + } + } namespaceTypes.push(namespace); }, }); @@ -200,11 +273,20 @@ export function extractRefDocs( typeMapping.set(type, refDoc); (array as any).push(refDoc); } + + function isIncludedBySourceFilter(type: Type): boolean { + if (!options.sourceFilter) return true; + const loc = getSourceLocation(type); + if (loc.isSynthetic) return true; + return options.sourceFilter(loc.file.path); + } + navigateTypesInNamespace( namespace, { decorator(dec) { if (hasInternalModifier(dec)) return; + if (!isIncludedBySourceFilter(dec)) return; collectType(dec, extractDecoratorRefDoc(program, dec), namespaceDoc.decorators); }, operation(operation) { @@ -212,6 +294,7 @@ export function extractRefDocs( if (!isDeclaredType(operation)) { return; } + if (!isIncludedBySourceFilter(operation)) return; if (operation.interface === undefined) { collectType( @@ -226,6 +309,7 @@ export function extractRefDocs( if (!isDeclaredType(iface)) { return; } + if (!isIncludedBySourceFilter(iface)) return; collectType(iface, extractInterfaceRefDocs(program, iface), namespaceDoc.interfaces); }, model(model) { @@ -236,6 +320,7 @@ export function extractRefDocs( if (model.name === "") { return; } + if (!isIncludedBySourceFilter(model)) return; collectType(model, extractModelRefDocs(program, model), namespaceDoc.models); }, enum(e) { @@ -243,6 +328,7 @@ export function extractRefDocs( if (!isDeclaredType(e)) { return; } + if (!isIncludedBySourceFilter(e)) return; collectType(e, extractEnumRefDoc(program, e), namespaceDoc.enums); }, union(union) { @@ -251,11 +337,13 @@ export function extractRefDocs( return; } if (union.name !== undefined) { + if (!isIncludedBySourceFilter(union)) return; collectType(union, extractUnionRefDocs(program, union as any), namespaceDoc.unions); } }, scalar(scalar) { if (hasInternalModifier(scalar)) return; + if (!isIncludedBySourceFilter(scalar)) return; collectType(scalar, extractScalarRefDocs(program, scalar), namespaceDoc.scalars); }, }, @@ -263,8 +351,20 @@ export function extractRefDocs( ); } - sort(namespaces); - for (const namespace of namespaces) { + // Remove namespaces that have no content after filtering + const filteredNamespaces = namespaces.filter( + (ns) => + ns.decorators.length > 0 || + ns.operations.length > 0 || + ns.interfaces.length > 0 || + ns.models.length > 0 || + ns.enums.length > 0 || + ns.unions.length > 0 || + ns.scalars.length > 0, + ); + + sort(filteredNamespaces); + for (const namespace of filteredNamespaces) { sort(namespace.decorators); sort(namespace.enums); sort(namespace.interfaces); @@ -279,7 +379,7 @@ export function extractRefDocs( } return diagnostics.wrap({ - namespaces, + namespaces: filteredNamespaces, getNamedTypeRefDoc: (type) => typeMapping.get(type), }); } @@ -525,6 +625,25 @@ function extractUnionRefDocs(program: Program, type: Union & { name: string }): templateParameters: extractTemplateParameterDocs(program, type), doc: doc, examples: extractExamples(type), + variants: new Map( + [...type.variants.values()] + .filter((v): v is UnionVariant & { name: string } => typeof v.name === "string") + .map((v) => [v.name, extractUnionVariantRefDocs(program, v)]), + ), + }; +} + +function extractUnionVariantRefDocs( + program: Program, + type: UnionVariant & { name: string }, +): UnionVariantRefDoc { + const doc = extractMainDoc(program, type); + return { + ...extractBase(program, type), + signature: getTypeSignature(type), + type, + doc: doc, + examples: extractExamples(type), }; } @@ -658,16 +777,133 @@ function extractEmitterOptionsRefDoc( options: JSONSchemaType>, ): EmitterOptionRefDoc[] { return Object.entries(options.properties).map(([name, value]: [string, any]) => { - return { - name, - type: value.enum - ? value.enum.map((x: string | number) => (typeof x === "string" ? `"${x}"` : x)).join(" | ") - : value.type, - doc: value.description ?? "", - }; + return extractEmitterOptionInfo(name, value); }); } +function extractEmitterOptionInfo(name: string, prop: any): EmitterOptionRefDoc { + // Handle oneOf: extract variants + if (prop.oneOf) { + return extractOneOfEmitterOption(name, prop); + } + + const option: Mutable = { + name, + type: resolveEmitterOptionType(prop), + doc: resolveDescription(prop.description), + }; + + if (prop.enum) { + option.allowedValues = prop.enum.map((x: string | number) => + typeof x === "string" ? `"${x}"` : String(x), + ); + } else if (prop.type === "array" && prop.items?.enum) { + option.allowedValues = prop.items.enum.map((x: string | number) => + typeof x === "string" ? `"${x}"` : String(x), + ); + } + + if (prop.default !== undefined) { + option.default = JSON.stringify(prop.default); + } + + if (prop.deprecated !== undefined) { + option.deprecated = typeof prop.deprecated === "string" ? prop.deprecated : ""; + } + if (prop.type === "object" && prop.properties) { + option.nestedOptions = Object.entries(prop.properties).map( + ([subName, subProp]: [string, any]) => extractEmitterOptionInfo(subName, subProp), + ); + } + + return option; +} + +function extractOneOfEmitterOption(name: string, prop: any): EmitterOptionRefDoc { + const rawVariants: any[] = prop.oneOf; + + const variants: EmitterOptionVariantRefDoc[] = []; + + for (const variant of rawVariants) { + const v: Mutable = { + type: resolveEmitterOptionType(variant), + }; + + if (variant.enum) { + v.allowedValues = variant.enum.map((x: string | number) => + typeof x === "string" ? `"${x}"` : String(x), + ); + } + + if (variant.default !== undefined) { + v.default = JSON.stringify(variant.default); + } + + if (variant.description) { + v.doc = resolveDescription(variant.description); + } + + if (variant.type === "object" && variant.properties) { + v.nestedOptions = Object.entries(variant.properties).map( + ([subName, subProp]: [string, any]) => extractEmitterOptionInfo(subName, subProp), + ); + } + + variants.push(v); + } + + const option: Mutable = { + name, + type: rawVariants.map((v: any) => resolveEmitterOptionType(v)).join(" | "), + doc: resolveDescription(prop.description), + variants, + }; + + if (prop.default !== undefined) { + option.default = JSON.stringify(prop.default); + } + + return option; +} + +function resolveEmitterOptionType(prop: any): string { + if (prop.oneOf) { + return prop.oneOf.map((s: any) => resolveEmitterOptionType(s)).join(" | "); + } + + if (prop.type === "array") { + if (prop.items) { + if (prop.items.enum) { + const values = prop.items.enum + .map((x: string | number) => (typeof x === "string" ? `"${x}"` : String(x))) + .join(" | "); + return `(${values})[]`; + } + const itemType = prop.items.type ?? "unknown"; + return `${itemType}[]`; + } + return "array"; + } + + if (prop.type === "object" && prop.properties) { + const keys = Object.keys(prop.properties); + return `object { ${keys.join(", ")} }`; + } + + if (prop.enum) { + return prop.enum + .map((x: string | number) => (typeof x === "string" ? `"${x}"` : String(x))) + .join(" | "); + } + + return prop.type ?? "unknown"; +} + +function resolveDescription(description: string | string[] | undefined): string { + if (description === undefined) return ""; + return Array.isArray(description) ? description.join("\n") : description; +} + function extractLinterRefDoc(libName: string, linter: LinterResolvedDefinition): LinterRefDoc { return { ruleSets: linter.ruleSets && extractLinterRuleSetsRefDoc(libName, linter.ruleSets), diff --git a/packages/tspd/src/ref-doc/types.ts b/packages/tspd/src/ref-doc/types.ts index b150882ef64..7929a9a2d4a 100644 --- a/packages/tspd/src/ref-doc/types.ts +++ b/packages/tspd/src/ref-doc/types.ts @@ -13,6 +13,7 @@ import type { Scalar, Type, Union, + UnionVariant, } from "@typespec/compiler"; export type TypeSpecRefDoc = TypeSpecLibraryRefDoc; @@ -37,6 +38,14 @@ export type TypeSpecLibraryRefDoc = TypeSpecRefDocBase & { /** Documentation about the linter rules and ruleset provided in this library. */ readonly linter?: LinterRefDoc; + + /** Documentation for sub-exports (e.g., "./streams", "./testing"). Keyed by export path. */ + readonly subExports?: ReadonlyMap; +}; + +export type SubExportRefDoc = TypeSpecRefDocBase & { + /** The export path (e.g., "./streams") */ + readonly path: string; }; export type TypeSpecRefDocBase = { @@ -69,6 +78,23 @@ export type EmitterOptionRefDoc = { readonly name: string; readonly type: string; readonly doc: string; + readonly default?: string; + readonly allowedValues?: readonly string[]; + /** If set, the option is deprecated. Contains the deprecation message or empty string. */ + readonly deprecated?: string; + /** Nested options for object-typed options. */ + readonly nestedOptions?: readonly EmitterOptionRefDoc[]; + /** When present, this option is a union of multiple variants (oneOf). */ + readonly variants?: readonly EmitterOptionVariantRefDoc[]; +}; + +export type EmitterOptionVariantRefDoc = { + readonly type: string; + readonly doc?: string; + readonly default?: string; + readonly allowedValues?: readonly string[]; + /** Nested options for object-typed variants. */ + readonly nestedOptions?: readonly EmitterOptionRefDoc[]; }; export type RefDocEntity = @@ -182,6 +208,11 @@ export type UnionRefDoc = NamedTypeRefDoc & { readonly type: Union; readonly templateParameters?: readonly TemplateParameterRefDoc[]; + readonly variants: ReadonlyMap; +}; + +export type UnionVariantRefDoc = NamedTypeRefDoc & { + readonly type: UnionVariant; }; export type ScalarRefDoc = NamedTypeRefDoc & { diff --git a/website/src/content/docs/docs/emitters/clients/http-client-js/reference/emitter.md b/website/src/content/docs/docs/emitters/clients/http-client-js/reference/emitter.md index 26247a08933..1c62897fe35 100644 --- a/website/src/content/docs/docs/emitters/clients/http-client-js/reference/emitter.md +++ b/website/src/content/docs/docs/emitters/clients/http-client-js/reference/emitter.md @@ -40,4 +40,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/website/src/content/docs/docs/emitters/json-schema/reference/emitter.md b/website/src/content/docs/docs/emitters/json-schema/reference/emitter.md index 9951ce71bfc..609cc318b8f 100644 --- a/website/src/content/docs/docs/emitters/json-schema/reference/emitter.md +++ b/website/src/content/docs/docs/emitters/json-schema/reference/emitter.md @@ -73,6 +73,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` @@ -81,6 +83,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/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md index a15c4c7e603..f1c6d3539bf 100644 --- a/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md +++ b/website/src/content/docs/docs/emitters/openapi3/reference/emitter.md @@ -38,10 +38,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` @@ -80,12 +85,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` @@ -99,6 +108,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. @@ -106,6 +117,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` @@ -117,6 +130,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` @@ -132,4 +147,21 @@ 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. | diff --git a/website/src/content/docs/docs/emitters/servers/http-server-csharp/reference/emitter.md b/website/src/content/docs/docs/emitters/servers/http-server-csharp/reference/emitter.md index 18f71229d4d..f13b5543953 100644 --- a/website/src/content/docs/docs/emitters/servers/http-server-csharp/reference/emitter.md +++ b/website/src/content/docs/docs/emitters/servers/http-server-csharp/reference/emitter.md @@ -46,36 +46,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` @@ -94,4 +106,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/website/src/content/docs/docs/emitters/servers/http-server-js/reference/emitter.md b/website/src/content/docs/docs/emitters/servers/http-server-js/reference/emitter.md index 98148e30624..84d22500d7f 100644 --- a/website/src/content/docs/docs/emitters/servers/http-server-js/reference/emitter.md +++ b/website/src/content/docs/docs/emitters/servers/http-server-js/reference/emitter.md @@ -40,6 +40,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. @@ -48,16 +50,22 @@ 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. diff --git a/website/src/content/docs/docs/libraries/http/reference/streams/data-types.md b/website/src/content/docs/docs/libraries/http/reference/streams/data-types.md new file mode 100644 index 00000000000..726f2e47aa0 --- /dev/null +++ b/website/src/content/docs/docs/libraries/http/reference/streams/data-types.md @@ -0,0 +1,74 @@ +--- +title: "Data types (streams)" +description: "Data types exported by @typespec/http/streams" +llmstxt: true +--- + +## TypeSpec.Http.Streams + +### `HttpStream` {#TypeSpec.Http.Streams.HttpStream} + +Defines a model that represents a stream protocol type whose data is described +by `Type`. + +The `ContentType` and `BodyType` describe how the stream is encoded over the wire, +while `Type` describes the data that the stream contains. + +```typespec +model TypeSpec.Http.Streams.HttpStream +``` + +#### Template Parameters + +| Name | Description | +| ----------- | --------------------------------------- | +| Type | The type of the stream's data. | +| ContentType | The content type of the stream. | +| BodyType | The underlying wire type of the stream. | + +#### Properties + +| Name | Type | Description | +| ----------- | ---------- | ----------- | +| contentType | `string` | | +| body | `BodyType` | | + +### `JsonlStream` {#TypeSpec.Http.Streams.JsonlStream} + +Describes a stream of JSON data with one JSON object per line and sets +the content type to `application/jsonl`. + +The JSON data is described by `Type`. + +```typespec +model TypeSpec.Http.Streams.JsonlStream +``` + +#### Template Parameters + +| Name | Description | +| ---- | --------------------------------------------------------- | +| Type | The set of models describing the JSON data in the stream. | + +#### Examples + +```typespec +model Message { + id: string; + text: string; +} + +@TypeSpec.Events.events +union Events { + Message, +} + +op subscribe(): JsonlStream; +``` + +#### Properties + +| Name | Type | Description | +| ----------- | --------------------- | ----------- | +| contentType | `"application/jsonl"` | | +| body | `string` | |