From 11592597095dc90ab02118ac450ce4f1fcada522 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 24 Jul 2026 11:28:26 -0400 Subject: [PATCH] Replace @microsoft/api-extractor with TypeDoc api-extractor is aging, lightly maintained, and does not support modern ESM export maps. Replace its two uses in the core packages with TypeDoc: - tspd typekit reference doc generation now builds its model from TypeDoc instead of @microsoft/api-extractor-model (removes 4 @microsoft/* deps). - Public-API documentation enforcement (previously api-extractor's ae-undocumented check) is now a TypeDoc-based eng/tsp-core/scripts/ check-api-docs.ts script, wired into build for json-schema, openapi, xml. Removes api-extractor.base.json, the 8 per-package api-extractor.json configs, and the @microsoft/api-extractor* catalog/root dependencies. --- ...tractor-with-typedoc-2026-7-23-16-30-12.md | 14 + ...typedoc-typekit-docs-2026-7-23-16-30-11.md | 7 + api-extractor.base.json | 55 --- eng/tsp-core/scripts/check-api-docs.ts | 161 ++++++++ package.json | 2 +- packages/asset-emitter/api-extractor.json | 4 - packages/asset-emitter/package.json | 2 +- packages/compiler/api-extractor.json | 4 - packages/compiler/package.json | 2 +- packages/graphql/api-extractor.json | 4 - packages/http/api-extractor.json | 4 - packages/http/package.json | 2 +- packages/json-schema/api-extractor.json | 4 - packages/json-schema/package.json | 4 +- packages/openapi/api-extractor.json | 4 - packages/openapi/package.json | 4 +- packages/openapi3/api-extractor.json | 4 - packages/openapi3/package.json | 2 +- packages/tspd/package.json | 4 - packages/tspd/src/ref-doc/api-extractor.ts | 87 ----- .../diagnosable-typekit-function.tsx | 55 +-- .../tspd/src/ref-doc/components/tsdoc.tsx | 62 --- .../ref-doc/components/typekit-function.tsx | 6 +- .../ref-doc/components/typekit-section.tsx | 5 +- packages/tspd/src/ref-doc/typekit-docs.ts | 168 +-------- packages/tspd/src/ref-doc/typekit-model.ts | 357 ++++++++++++++++++ packages/xml/api-extractor.json | 4 - packages/xml/package.json | 4 +- pnpm-lock.yaml | 67 ++-- pnpm-workspace.yaml | 4 - .../libraries/http/reference/typekits.mdx | 180 +++++---- .../standard-library/reference/typekits.mdx | 66 +--- 32 files changed, 715 insertions(+), 637 deletions(-) create mode 100644 .chronus/changes/replace-api-extractor-with-typedoc-2026-7-23-16-30-12.md create mode 100644 .chronus/changes/tspd-typedoc-typekit-docs-2026-7-23-16-30-11.md delete mode 100644 api-extractor.base.json create mode 100644 eng/tsp-core/scripts/check-api-docs.ts delete mode 100644 packages/asset-emitter/api-extractor.json delete mode 100644 packages/compiler/api-extractor.json delete mode 100644 packages/graphql/api-extractor.json delete mode 100644 packages/http/api-extractor.json delete mode 100644 packages/json-schema/api-extractor.json delete mode 100644 packages/openapi/api-extractor.json delete mode 100644 packages/openapi3/api-extractor.json delete mode 100644 packages/tspd/src/ref-doc/api-extractor.ts delete mode 100644 packages/tspd/src/ref-doc/components/tsdoc.tsx create mode 100644 packages/tspd/src/ref-doc/typekit-model.ts delete mode 100644 packages/xml/api-extractor.json diff --git a/.chronus/changes/replace-api-extractor-with-typedoc-2026-7-23-16-30-12.md b/.chronus/changes/replace-api-extractor-with-typedoc-2026-7-23-16-30-12.md new file mode 100644 index 00000000000..0a9ee6696d1 --- /dev/null +++ b/.chronus/changes/replace-api-extractor-with-typedoc-2026-7-23-16-30-12.md @@ -0,0 +1,14 @@ +--- +changeKind: internal +packages: + - "@typespec/compiler" + - "@typespec/http" + - "@typespec/openapi" + - "@typespec/openapi3" + - "@typespec/json-schema" + - "@typespec/xml" + - "@typespec/asset-emitter" + - "@typespec/graphql" +--- + +Replace the `@microsoft/api-extractor` public API documentation enforcement with a TypeDoc-based `check-api-docs` check that supports modern ESM export maps and validates every stable public entry point. diff --git a/.chronus/changes/tspd-typedoc-typekit-docs-2026-7-23-16-30-11.md b/.chronus/changes/tspd-typedoc-typekit-docs-2026-7-23-16-30-11.md new file mode 100644 index 00000000000..881d1a1541b --- /dev/null +++ b/.chronus/changes/tspd-typedoc-typekit-docs-2026-7-23-16-30-11.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/tspd" +--- + +Migrate typekit reference documentation generation from `@microsoft/api-extractor` to TypeDoc, removing the `@microsoft/api-extractor`, `@microsoft/api-extractor-model`, `@microsoft/tsdoc` and `@microsoft/tsdoc-config` dependencies. diff --git a/api-extractor.base.json b/api-extractor.base.json deleted file mode 100644 index c5e45a67c72..00000000000 --- a/api-extractor.base.json +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "mainEntryPointFilePath": "/dist/src/index.d.ts", - "apiReport": { - "enabled": true, - "reportFolder": "/temp" - }, - "docModel": { - "enabled": true - }, - "dtsRollup": { - "enabled": false - }, - "messages": { - "compilerMessageReporting": { - "default": { - "logLevel": "error" - }, - "TS2552": { - "logLevel": "none", // False positive with non support for 5.6 yet - "addToApiReportFile": false - } - }, - - "extractorMessageReporting": { - "default": { - "logLevel": "error" - }, - "ae-undocumented": { - "logLevel": "error" - }, - "ae-forgotten-export": { - "logLevel": "none", // False positive with decorators functions. - "addToApiReportFile": false - }, - "ae-missing-release-tag": { - "logLevel": "none", // This is just extra verbosity. Should rely on api view to see if we exported something by mistake - "addToApiReportFile": false - } - }, - - "tsdocMessageReporting": { - "default": { - "logLevel": "error" - }, - "tsdoc-param-tag-missing-hyphen": { - "logLevel": "none", - "addToApiReportFile": false - } - } - } -} diff --git a/eng/tsp-core/scripts/check-api-docs.ts b/eng/tsp-core/scripts/check-api-docs.ts new file mode 100644 index 00000000000..207c7bc6465 --- /dev/null +++ b/eng/tsp-core/scripts/check-api-docs.ts @@ -0,0 +1,161 @@ +#!/usr/bin/env tsx +/* eslint-disable no-console */ +// Enforces that the public API of a package is documented, replacing the +// `ae-undocumented` check previously provided by `@microsoft/api-extractor`. +// +// Unlike api-extractor (which only supported a single entry point), this uses +// TypeDoc and validates every *stable* public entry point declared in the +// package `exports` map. Experimental and testing entry points are excluded. +import { existsSync, readFileSync } from "fs"; +import { join, resolve } from "path"; +import { Application, LogLevel } from "typedoc"; +import { fileURLToPath } from "url"; + +/** Entry point path segments that are not subject to documentation enforcement. */ +const EXCLUDED_ENTRYPOINT_SEGMENTS = ["experimental", "testing", "internal"]; + +/** Reflection kinds that must be documented when part of the public API. */ +// Reflection kinds that must be documented when part of the public API. This +// list mirrors the effective surface of api-extractor's `ae-undocumented` check: +// packages that previously enforced it report zero findings here. `Property`, +// `Variable` and `EnumMember` are intentionally omitted so the check does not +// descend into object-literal internals (e.g. the diagnostic messages defined +// on a library's `$lib` const), which api-extractor never enforced. +const REQUIRED_TO_BE_DOCUMENTED = [ + "Interface", + "Function", + "Class", + "Method", + "Enum", + "TypeAlias", + "Accessor", +]; + +interface PackageJson { + name?: string; + exports?: Record; +} + +function resolveTypesPath(entry: unknown): string | undefined { + if (typeof entry === "string") { + return entry.replace(/\.js$/, ".d.ts"); + } + if (entry && typeof entry === "object") { + const obj = entry as Record; + if (typeof obj.types === "string") { + return obj.types; + } + for (const condition of ["import", "default"]) { + const resolved = resolveTypesPath(obj[condition]); + if (resolved) { + return resolved; + } + } + } + return undefined; +} + +function isExcluded(exportKey: string): boolean { + const segments = exportKey.split("/"); + return segments.some((segment) => EXCLUDED_ENTRYPOINT_SEGMENTS.includes(segment)); +} + +/** Resolve the stable public source entry points from the package `exports` map. */ +function resolveEntryPoints(packageDir: string, pkgJson: PackageJson): string[] { + const exports = pkgJson.exports; + if (!exports) { + // Fall back to the conventional main entry point. + const fallback = join(packageDir, "src", "index.ts"); + return existsSync(fallback) ? [fallback] : []; + } + + const entryPoints = new Set(); + for (const [key, value] of Object.entries(exports)) { + if (isExcluded(key)) { + continue; + } + const typesPath = resolveTypesPath(value); + if (!typesPath) { + continue; + } + // e.g. ./dist/src/index.d.ts -> src/index.ts + const sourceRelative = typesPath.replace(/^\.\/dist\//, "./").replace(/\.d\.ts$/, ".ts"); + const sourcePath = resolve(packageDir, sourceRelative); + if (existsSync(sourcePath)) { + entryPoints.add(sourcePath); + } + } + return [...entryPoints]; +} + +async function main() { + const packageDir = resolve(process.cwd(), process.argv[2] ?? "."); + const pkgJsonPath = join(packageDir, "package.json"); + if (!existsSync(pkgJsonPath)) { + console.error(`No package.json found at ${pkgJsonPath}`); + process.exit(1); + } + const pkgJson: PackageJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8")); + const entryPoints = resolveEntryPoints(packageDir, pkgJson); + if (entryPoints.length === 0) { + console.error(`No public entry points found for ${pkgJson.name ?? packageDir}`); + process.exit(1); + } + + const app = await Application.bootstrapWithPlugins({ + entryPoints, + entryPointStrategy: "resolve", + tsconfig: join(packageDir, "tsconfig.build.json"), + excludeInternal: true, + readme: "none", + logLevel: LogLevel.Error, + requiredToBeDocumented: REQUIRED_TO_BE_DOCUMENTED as never, + validation: { + notDocumented: true, + invalidLink: false, + notExported: false, + rewrittenLink: false, + unusedMergeModuleWith: false, + }, + }); + + // With `notExported`/`invalidLink` disabled, the only validation warnings are + // the "does not have any documentation" ones, so capturing them here isolates + // undocumented public API from unrelated TypeDoc noise. + const undocumented: string[] = []; + const originalValidationWarning = app.logger.validationWarning.bind(app.logger); + app.logger.validationWarning = ((text: string, ...rest: unknown[]) => { + undocumented.push(String(text)); + return (originalValidationWarning as (...args: unknown[]) => void)(text, ...rest); + }) as typeof app.logger.validationWarning; + + const project = await app.convert(); + if (!project) { + console.error("TypeDoc failed to analyze the package (see errors above)."); + process.exit(1); + } + app.validate(project); + + if (undocumented.length > 0) { + console.error( + `\n${undocumented.length} public API symbol(s) in ${pkgJson.name ?? packageDir} are missing documentation:\n`, + ); + for (const message of undocumented) { + console.error(` - ${message}`); + } + console.error(""); + process.exit(1); + } + + console.log( + `✔ All public API of ${pkgJson.name ?? packageDir} is documented (${entryPoints.length} entry point(s)).`, + ); +} + +// Allow running directly via tsx. +if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) { + main().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/package.json b/package.json index 9835abd7cbf..0aa86a2fd80 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "@chronus/chronus": "catalog:", "@chronus/github": "catalog:", "@chronus/github-pr-commenter": "catalog:", - "@microsoft/api-extractor": "catalog:", "@octokit/core": "catalog:", "@octokit/plugin-paginate-graphql": "catalog:", "@octokit/plugin-rest-endpoint-methods": "catalog:", @@ -66,6 +65,7 @@ "rimraf": "catalog:", "tsx": "catalog:", "turbo": "catalog:", + "typedoc": "catalog:", "typescript": "catalog:", "vitest": "catalog:", "yaml": "catalog:" diff --git a/packages/asset-emitter/api-extractor.json b/packages/asset-emitter/api-extractor.json deleted file mode 100644 index 2069b8ac37f..00000000000 --- a/packages/asset-emitter/api-extractor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../api-extractor.base.json" -} diff --git a/packages/asset-emitter/package.json b/packages/asset-emitter/package.json index eab855f5fad..f27daf9df0a 100644 --- a/packages/asset-emitter/package.json +++ b/packages/asset-emitter/package.json @@ -34,7 +34,7 @@ "test:ui": "vitest --ui", "lint": "oxlint . --deny-warnings", "lint:fix": "oxlint . --fix", - "api-extractor": "api-extractor run --local --verbose" + "check-api-docs": "tsx ../../eng/tsp-core/scripts/check-api-docs.ts ." }, "files": [ "lib/*.tsp", diff --git a/packages/compiler/api-extractor.json b/packages/compiler/api-extractor.json deleted file mode 100644 index 2069b8ac37f..00000000000 --- a/packages/compiler/api-extractor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../api-extractor.base.json" -} diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 523cd2b2862..5cc435c13f6 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -86,7 +86,7 @@ "clean": "rimraf ./dist ./temp", "build:init-templates-index": "tsx ./.scripts/build-init-templates.ts", "build": "pnpm gen-manifest && pnpm build:init-templates-index && pnpm compile && pnpm generate-tmlanguage", - "api-extractor": "api-extractor run --local --verbose", + "check-api-docs": "tsx ../../eng/tsp-core/scripts/check-api-docs.ts .", "compile": "tsc -p tsconfig.build.json", "watch": "tsc -p tsconfig.build.json --watch", "watch-tmlanguage": "node scripts/watch-tmlanguage.js", diff --git a/packages/graphql/api-extractor.json b/packages/graphql/api-extractor.json deleted file mode 100644 index 2069b8ac37f..00000000000 --- a/packages/graphql/api-extractor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../api-extractor.base.json" -} diff --git a/packages/http/api-extractor.json b/packages/http/api-extractor.json deleted file mode 100644 index 2069b8ac37f..00000000000 --- a/packages/http/api-extractor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../api-extractor.base.json" -} diff --git a/packages/http/package.json b/packages/http/package.json index 368611b2d53..d9f2a13d6ed 100644 --- a/packages/http/package.json +++ b/packages/http/package.json @@ -56,7 +56,7 @@ "scripts": { "clean": "rimraf ./dist ./temp", "build": "pnpm gen-extern-signature && tsc -p tsconfig.build.json && pnpm lint-typespec-library", - "api-extractor": "api-extractor run --local --verbose", + "check-api-docs": "tsx ../../eng/tsp-core/scripts/check-api-docs.ts .", "watch": "tsc -p tsconfig.build.json --watch", "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", diff --git a/packages/json-schema/api-extractor.json b/packages/json-schema/api-extractor.json deleted file mode 100644 index 2069b8ac37f..00000000000 --- a/packages/json-schema/api-extractor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../api-extractor.base.json" -} diff --git a/packages/json-schema/package.json b/packages/json-schema/package.json index 22e1db136d3..2d13e908984 100644 --- a/packages/json-schema/package.json +++ b/packages/json-schema/package.json @@ -36,7 +36,7 @@ }, "scripts": { "clean": "rimraf ./dist ./temp", - "build": "pnpm gen-extern-signature && tsc -p tsconfig.build.json && pnpm lint-typespec-library && pnpm api-extractor", + "build": "pnpm gen-extern-signature && tsc -p tsconfig.build.json && pnpm lint-typespec-library && pnpm check-api-docs", "watch": "tsc -p tsconfig.build.json --watch", "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", @@ -46,7 +46,7 @@ "lint": "oxlint . --deny-warnings", "lint:fix": "oxlint . --fix", "regen-docs": "tspd doc . --enable-experimental --llmstxt --output-dir ../../website/src/content/docs/docs/emitters/json-schema/reference", - "api-extractor": "api-extractor run --local --verbose" + "check-api-docs": "tsx ../../eng/tsp-core/scripts/check-api-docs.ts ." }, "files": [ "lib/*.tsp", diff --git a/packages/openapi/api-extractor.json b/packages/openapi/api-extractor.json deleted file mode 100644 index 2069b8ac37f..00000000000 --- a/packages/openapi/api-extractor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../api-extractor.base.json" -} diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 6583344f417..2c39f608e9d 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -35,7 +35,7 @@ }, "scripts": { "clean": "rimraf ./dist ./temp", - "build": "pnpm gen-extern-signature && tsc -p tsconfig.build.json && pnpm lint-typespec-library && pnpm api-extractor", + "build": "pnpm gen-extern-signature && tsc -p tsconfig.build.json && pnpm lint-typespec-library && pnpm check-api-docs", "watch": "tsc -p tsconfig.build.json --watch", "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", @@ -46,7 +46,7 @@ "lint": "oxlint . --deny-warnings", "lint:fix": "oxlint . --fix", "regen-docs": "tspd doc . --enable-experimental --llmstxt --output-dir ../../website/src/content/docs/docs/libraries/openapi/reference", - "api-extractor": "api-extractor run --local --verbose" + "check-api-docs": "tsx ../../eng/tsp-core/scripts/check-api-docs.ts ." }, "files": [ "lib/*.tsp", diff --git a/packages/openapi3/api-extractor.json b/packages/openapi3/api-extractor.json deleted file mode 100644 index 2069b8ac37f..00000000000 --- a/packages/openapi3/api-extractor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../api-extractor.base.json" -} diff --git a/packages/openapi3/package.json b/packages/openapi3/package.json index 01bd3bd063b..f6cbb86b7bd 100644 --- a/packages/openapi3/package.json +++ b/packages/openapi3/package.json @@ -55,7 +55,7 @@ "regen-docs": "tspd doc . --enable-experimental --llmstxt --output-dir ../../website/src/content/docs/docs/emitters/openapi3/reference", "regen-specs": "cross-env RECORD=true vitest run", "gen-version": "node scripts/generate-version.js", - "api-extractor": "api-extractor run --local --verbose" + "check-api-docs": "tsx ../../eng/tsp-core/scripts/check-api-docs.ts ." }, "files": [ "lib/*.tsp", diff --git a/packages/tspd/package.json b/packages/tspd/package.json index 596136a384c..7c9586dde01 100644 --- a/packages/tspd/package.json +++ b/packages/tspd/package.json @@ -58,10 +58,6 @@ "@alloy-js/core": "catalog:", "@alloy-js/markdown": "catalog:", "@alloy-js/typescript": "catalog:", - "@microsoft/api-extractor": "catalog:", - "@microsoft/api-extractor-model": "catalog:", - "@microsoft/tsdoc": "catalog:", - "@microsoft/tsdoc-config": "catalog:", "@typespec/compiler": "workspace:^", "picocolors": "catalog:", "prettier": "catalog:", diff --git a/packages/tspd/src/ref-doc/api-extractor.ts b/packages/tspd/src/ref-doc/api-extractor.ts deleted file mode 100644 index e73af12bfa5..00000000000 --- a/packages/tspd/src/ref-doc/api-extractor.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Extractor, ExtractorConfig, ExtractorResult } from "@microsoft/api-extractor"; -import { ApiModel } from "@microsoft/api-extractor-model"; -import { TSDocConfigFile } from "@microsoft/tsdoc-config"; -import { joinPaths, type PackageJson } from "@typespec/compiler"; - -export async function createApiModel(libraryPath: string, pkgJson: PackageJson) { - const apiModel: ApiModel = new ApiModel(); - - const typekitexports = Object.keys(pkgJson.exports!).filter((x) => x.includes("typekit")); - if (typekitexports.length === 0) { - return undefined; - } - - for (const exportName of [typekitexports[0]]) { - const modelFilePath = createApiModelFileForExport(libraryPath, pkgJson, exportName); - apiModel.loadPackage(joinPaths(modelFilePath)); - } - - return apiModel; -} - -const knownConditions = ["import", "default"]; - -function resolveDefinitionFilePath( - libraryPath: string, - pkgJson: PackageJson, - exportName: string, -): string { - const obj = (pkgJson.exports as any)?.[exportName] as any; - if (!obj) { - throw new Error(`Cannot resolve definition file path for export ${exportName}`); - } - - if (typeof obj === "string") { - return joinPaths(libraryPath, obj).replace(/\.js$/, ".d.ts"); - } - if (typeof obj === "object" && "types" in obj) { - return joinPaths(libraryPath, obj.types); - } - for (const condition of knownConditions) { - if (condition in obj) { - const conditionObj = obj[condition]; - if (typeof conditionObj === "object" && "types" in conditionObj) { - return joinPaths(libraryPath, conditionObj.types).replace(/\.js$/, ".d.ts"); - } - } - } - throw new Error(`Cannot resolve definition file path for export ${exportName}`); -} - -export function createApiModelFileForExport( - libraryPath: string, - pkgJson: PackageJson, - exportName: string, -): string { - const entrypoint = resolveDefinitionFilePath(libraryPath, pkgJson, exportName); - const modelFilePath = joinPaths(libraryPath, "temp", `${exportName.slice(2)}.api.json`); - const config = ExtractorConfig.prepare({ - configObject: { - mainEntryPointFilePath: entrypoint, - compiler: { - tsconfigFilePath: joinPaths(libraryPath, "tsconfig.build.json"), - }, - docModel: { - enabled: true, - apiJsonFilePath: modelFilePath, - }, - projectFolder: libraryPath, - }, - configObjectFullPath: undefined, - tsdocConfigFile: TSDocConfigFile.loadFromObject({ - $schema: "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", - tagDefinitions: [ - { - tagName: "@typekit", - syntaxKind: "block", - }, - ], - }), - packageJsonFullPath: joinPaths(libraryPath, "package.json"), - }); - const extractorResult: ExtractorResult = Extractor.invoke(config); - if (!extractorResult.succeeded) { - throw new Error("API Extractor failed to generate the API model"); - } - return modelFilePath; -} diff --git a/packages/tspd/src/ref-doc/components/diagnosable-typekit-function.tsx b/packages/tspd/src/ref-doc/components/diagnosable-typekit-function.tsx index a1d8b9d9b91..e238edf479b 100644 --- a/packages/tspd/src/ref-doc/components/diagnosable-typekit-function.tsx +++ b/packages/tspd/src/ref-doc/components/diagnosable-typekit-function.tsx @@ -1,7 +1,6 @@ import { List } from "@alloy-js/core"; import * as md from "@alloy-js/markdown"; import * as ts from "@alloy-js/typescript"; -import { Excerpt, ExcerptToken } from "@microsoft/api-extractor-model"; import { TypekitFunctionDoc } from "../typekit-docs.js"; export interface DiagnosableTypekitFunctionProps { @@ -10,7 +9,10 @@ export interface DiagnosableTypekitFunctionProps { export function DiagnosableTypekitFunction(props: DiagnosableTypekitFunctionProps) { const path = [`$(program)`, ...props.typekit.path]; - const { parameters, returnType } = parseDiagnosableExcerpt(props.typekit.excerpt); + const params = (props.typekit.parameters ?? []).map( + (x) => `${x.name}${x.optional ? "?" : ""}: ${x.type}`, + ); + const returnType = props.typekit.returnType; const accessorParts = path.map((x) => ); @@ -18,9 +20,9 @@ export function DiagnosableTypekitFunction(props: DiagnosableTypekitFunctionProp <> {accessorParts} - + - {`: ${renderTokens(returnType)};`} + {`: ${returnType};`} ); const fnDiagSig = ( @@ -28,16 +30,16 @@ export function DiagnosableTypekitFunction(props: DiagnosableTypekitFunctionProp {accessorParts} - + - {`: [${renderTokens(returnType)}, readonly Diagnostic[]];`} + {`: [${returnType}, readonly Diagnostic[]];`} ); return ( `}> - {props.typekit.docComment?.emitAsTsdoc().trimEnd()} + {props.typekit.tsdoc} {fnSig} {fnDiagSig} @@ -45,42 +47,3 @@ export function DiagnosableTypekitFunction(props: DiagnosableTypekitFunctionProp ); } - -function renderTokens(tokens: ExcerptToken[]): string { - return tokens.map((x) => x.text).join(""); -} - -const ARROW_FN_SEPARATOR = ") => "; -function parseDiagnosableExcerpt(excerpt: Excerpt): { - parameters: ExcerptToken[]; - returnType: ExcerptToken[]; -} { - const tokens = excerpt.spannedTokens.slice(1); // remove the function name Diagnosable - tokens[0] = new ExcerptToken(tokens[0].kind, tokens[0].text.slice(2)); // remove <( - tokens[tokens.length - 1] = new ExcerptToken( - tokens[tokens.length - 1].kind, - tokens[tokens.length - 1].text.slice(0, -1), - ); // remove > - const splitIndex = tokens.findIndex((x) => x.text.includes(ARROW_FN_SEPARATOR)); - if (splitIndex !== -1) { - if (tokens[splitIndex].text === ARROW_FN_SEPARATOR) { - const paramsTokens = tokens.slice(0, splitIndex); - const returnTypeTokens = tokens.slice(splitIndex + 1); - return { - parameters: paramsTokens, - returnType: returnTypeTokens, - }; - } else { - tokens[splitIndex] = new ExcerptToken( - tokens[splitIndex].kind, - tokens[splitIndex].text.replace(ARROW_FN_SEPARATOR, ""), - ); - return { - parameters: tokens.slice(0, splitIndex), - returnType: tokens.slice(splitIndex), - }; - } - } else { - throw new Error(`Cannot parse Diagnosable function signature: ${excerpt.text}.`); - } -} diff --git a/packages/tspd/src/ref-doc/components/tsdoc.tsx b/packages/tspd/src/ref-doc/components/tsdoc.tsx deleted file mode 100644 index 8c153b12902..00000000000 --- a/packages/tspd/src/ref-doc/components/tsdoc.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { mapJoin } from "@alloy-js/core"; -import { - DocNodeKind, - DocNodeTransforms, - DocPlainText, - DocSection, - type DocNode, - type DocParagraph, -} from "@microsoft/tsdoc"; - -export interface TSDocProps { - readonly node: DocNode; -} - -export function TsDoc(props: TSDocProps) { - switch (props.node.kind) { - case DocNodeKind.Paragraph: - return ; - case DocNodeKind.Section: - return ; - case DocNodeKind.PlainText: - return ; - default: - return props.node.kind; - } -} - -export interface TsDocParagraphProps { - node: DocParagraph; -} -export function TsDocParagraph(props: TsDocParagraphProps) { - const trimmed = DocNodeTransforms.trimSpacesInParagraph(props.node); - let contentStartIndex = 0; - while ( - contentStartIndex < trimmed.nodes.length && - trimmed.nodes[contentStartIndex].kind === DocNodeKind.SoftBreak - ) { - contentStartIndex++; - } - - return trimmed.nodes.slice(contentStartIndex).map((node) => TsDoc({ node })); -} - -export interface TsDocPlainTextProps { - node: DocPlainText; -} -export function TsDocPlainText(props: TsDocPlainTextProps) { - return props.node.text; -} - -export interface TsDocSectionProps { - node: DocSection; -} -export function TsDocSection(props: TsDocSectionProps) { - return mapJoin( - () => props.node.nodes as DocNode[], - (node) => , - { - joiner: "\n\n", - }, - ); -} diff --git a/packages/tspd/src/ref-doc/components/typekit-function.tsx b/packages/tspd/src/ref-doc/components/typekit-function.tsx index f8d531fa86d..7d05f3a2e3d 100644 --- a/packages/tspd/src/ref-doc/components/typekit-function.tsx +++ b/packages/tspd/src/ref-doc/components/typekit-function.tsx @@ -18,14 +18,14 @@ export function TypekitFunction(props: TypekitFunctionProps) { <> ) : ( `${x.name}: ${x.typeExcerpt.text}`)} + args={props.typekit.parameters!.map((x) => `${x.name}${x.optional ? "?" : ""}: ${x.type}`)} /> ); return ( {"```ts"} - {props.typekit.docComment?.emitAsTsdoc().trimEnd()} + {props.typekit.tsdoc} <> {path.map((x) => ( @@ -33,7 +33,7 @@ export function TypekitFunction(props: TypekitFunctionProps) { ))} {sig} - <>: {props.typekit.returnTypeExcerpt.text}; + <>: {props.typekit.returnType}; {"```"} diff --git a/packages/tspd/src/ref-doc/components/typekit-section.tsx b/packages/tspd/src/ref-doc/components/typekit-section.tsx index 33c0b7ddb21..cf608879ce4 100644 --- a/packages/tspd/src/ref-doc/components/typekit-section.tsx +++ b/packages/tspd/src/ref-doc/components/typekit-section.tsx @@ -1,7 +1,6 @@ -import { For, Prose } from "@alloy-js/core"; +import { For } from "@alloy-js/core"; import * as md from "@alloy-js/markdown"; import { TypekitNamespace } from "../typekit-docs.js"; -import { TsDoc } from "./tsdoc.jsx"; import { TypekitFunction } from "./typekit-function.jsx"; export interface TypekitSectionProps { @@ -11,7 +10,7 @@ export interface TypekitSectionProps { export function TypekitSection(props: TypekitSectionProps) { return ( - {props.typekit.doc && } + {props.typekit.doc} {(x) => } diff --git a/packages/tspd/src/ref-doc/typekit-docs.ts b/packages/tspd/src/ref-doc/typekit-docs.ts index 83e63569334..8491c6c58d6 100644 --- a/packages/tspd/src/ref-doc/typekit-docs.ts +++ b/packages/tspd/src/ref-doc/typekit-docs.ts @@ -1,74 +1,21 @@ -import { - ApiDocumentedItem, - ApiInterface, - ApiMethodSignature, - ApiProperty, - ApiPropertySignature, - Excerpt, -} from "@microsoft/api-extractor-model"; -import { DocNode, type DocComment, type DocSection } from "@microsoft/tsdoc"; -import { joinPaths, PackageJson } from "@typespec/compiler"; +import { joinPaths } from "@typespec/compiler"; import { writeFile } from "fs/promises"; -import { createApiModel } from "./api-extractor.js"; import { createTypekitDocs } from "./components/typekits-file.js"; +import { createTypekitCollection } from "./typekit-model.js"; import { readPackageJson } from "./utils/misc.js"; -export interface TypekitCollection { - namespaces: Record; -} - -export interface TypekitNamespace { - name: string; - typeName: string; - doc?: DocSection; - entries: Record; -} - -type TypekitEntryDoc = TypekitNamespace | TypekitFunctionDoc; - -export interface TypekitFunctionDoc { - kind: "getter" | "method" | "diagnosable"; - /** - * Name of the function(Last part of the path) - * @example For `$(program).foo.bar.baz()` the name is `baz` - */ - name: string; - /** - * Full typekit path under $ - * @example For `$(program).foo.bar.baz()` the path is `["foo", "bar", "baz"]` - */ - path: string[]; - /** - * Parameters of the function. Undefined for getters. - */ - parameters?: TsFunctionParameter[]; - - /** - * Doc comment - */ - docComment?: DocComment; - /** - * The excerpt of the function - */ - excerpt: Excerpt; - - /** - * The excerpt of the return type - */ - returnTypeExcerpt: Excerpt; -} - -export interface TsFunctionParameter { - name: string; - doc: string; - optional: boolean; - typeExcerpt: Excerpt; -} +export type { + TsFunctionParameter, + TypekitCollection, + TypekitEntryDoc, + TypekitFunctionDoc, + TypekitNamespace, +} from "./typekit-model.js"; export async function writeTypekitDocs(libraryPath: string, outputDir: string): Promise { const pkgJson = await readPackageJson(libraryPath); - const typekits = await getTypekitApi(libraryPath, pkgJson); + const typekits = await createTypekitCollection(libraryPath, pkgJson); if (!typekits) { return; } @@ -77,98 +24,3 @@ export async function writeTypekitDocs(libraryPath: string, outputDir: string): await writeFile(joinPaths(outputDir, file), content); } } - -async function getTypekitApi( - libraryPath: string, - pkgJson: PackageJson, -): Promise { - const api = await createApiModel(libraryPath, pkgJson); - if (!api) { - return undefined; - } - const namespaces: Record = {}; - for (const pkgMember of api.packages[0].members) { - for (const member of pkgMember.members) { - if (member instanceof ApiInterface) { - const docComment: DocComment | undefined = (member as ApiDocumentedItem).tsdocComment; - const typekitTag = docComment?.customBlocks.find((x) => x.blockTag.tagName === "@typekit"); - if (typekitTag) { - const name = (typekitTag.content.nodes[0] as any).nodes.filter( - (x: DocNode) => x.kind === "PlainText", - )[0].text; - const typekit: TypekitNamespace = resolveTypekit(member, [name]); - namespaces[name] = typekit; - } - } - } - } - - function resolveTypekit(iface: ApiInterface, path: string[]): TypekitNamespace { - const typekit: TypekitNamespace = { - name: path[0], - typeName: iface.displayName, - doc: iface.tsdocComment?.summarySection, - entries: {}, - }; - for (const member of iface.members) { - if (member instanceof ApiPropertySignature) { - const propertyReference = member.propertyTypeExcerpt.spannedTokens[0].canonicalReference; - if (propertyReference) { - const subkit = api!.resolveDeclarationReference(propertyReference, member); - if (subkit.resolvedApiItem instanceof ApiInterface) { - typekit.entries[member.displayName] = resolveTypekit( - subkit.resolvedApiItem as ApiInterface, - [...path, member.displayName], - ); - } else if (propertyReference.toString() === "@typespec/compiler!Diagnosable:type") { - typekit.entries[member.displayName] = { - kind: "diagnosable", - name: member.displayName, - docComment: member.tsdocComment, - path: [...path, member.displayName], - excerpt: member.propertyTypeExcerpt, - returnTypeExcerpt: member.propertyTypeExcerpt, - }; - } else { - throw new Error( - `All typekits properties should be sub kits but got a ${subkit.resolvedApiItem?.kind} for ${path.join(".")}.${member.displayName}: ${subkit.errorMessage}`, - ); - } - } - } else if (member instanceof ApiMethodSignature) { - typekit.entries[member.displayName] = { - kind: "method", - name: member.displayName, - docComment: member.tsdocComment, - path: [...path, member.displayName], - parameters: member.parameters.map((param) => ({ - doc: "TODO doc", - name: param.name, - optional: param.isOptional, - typeExcerpt: param.parameterTypeExcerpt, - })), - excerpt: member.excerpt, - returnTypeExcerpt: member.returnTypeExcerpt, - }; - } else if (member instanceof ApiProperty) { - typekit.entries[member.displayName] = { - kind: "getter", - name: member.displayName, - docComment: member.tsdocComment, - path: [...path, member.displayName], - parameters: [], - excerpt: member.excerpt, - returnTypeExcerpt: member.propertyTypeExcerpt, - }; - } else { - // eslint-disable-next-line no-console - console.warn(`Unknown member: ${member.displayName} of kind ${member.kind}`); - } - } - return typekit; - } - - return { - namespaces, - }; -} diff --git a/packages/tspd/src/ref-doc/typekit-model.ts b/packages/tspd/src/ref-doc/typekit-model.ts new file mode 100644 index 00000000000..f3fd91296c0 --- /dev/null +++ b/packages/tspd/src/ref-doc/typekit-model.ts @@ -0,0 +1,357 @@ +import { joinPaths, type PackageJson } from "@typespec/compiler"; +import { + Application, + type Comment, + type CommentDisplayPart, + type DeclarationReflection, + ReflectionKind, + type SignatureReflection, + type SomeType, +} from "typedoc"; + +export interface TypekitCollection { + namespaces: Record; +} + +export interface TypekitNamespace { + kind: "namespace"; + name: string; + typeName: string; + /** Rendered summary of the typekit interface. */ + doc?: string; + entries: Record; +} + +export type TypekitEntryDoc = TypekitNamespace | TypekitFunctionDoc; + +export interface TypekitFunctionDoc { + kind: "getter" | "method" | "diagnosable"; + /** + * Name of the function(Last part of the path) + * @example For `$(program).foo.bar.baz()` the name is `baz` + */ + name: string; + /** + * Full typekit path under $ + * @example For `$(program).foo.bar.baz()` the path is `["foo", "bar", "baz"]` + */ + path: string[]; + /** + * Parameters of the function. Undefined for getters. + */ + parameters?: TsFunctionParameter[]; + /** + * Rendered tsdoc comment block (`/** ... */`). + */ + tsdoc?: string; + /** + * Rendered return type of the function/getter. + */ + returnType: string; +} + +export interface TsFunctionParameter { + name: string; + optional: boolean; + type: string; +} + +/** + * Build the typekit model for the given library using TypeDoc. + */ +export async function createTypekitCollection( + libraryPath: string, + pkgJson: PackageJson, +): Promise { + const entrypoint = resolveTypekitSourceEntrypoint(libraryPath, pkgJson); + if (!entrypoint) { + return undefined; + } + + const app = await Application.bootstrapWithPlugins({ + entryPoints: [entrypoint], + tsconfig: joinPaths(libraryPath, "tsconfig.build.json"), + entryPointStrategy: "resolve", + excludeExternals: false, + }); + + // Register the custom `@typekit` block tag so TypeDoc captures it. + const blockTags = app.options.getValue("blockTags") as `@${string}`[]; + if (!blockTags.includes("@typekit")) { + app.options.setValue("blockTags", [...blockTags, "@typekit"]); + } + + const project = await app.convert(); + if (!project) { + return undefined; + } + + const namespaces: Record = {}; + const entries: [string, DeclarationReflection][] = []; + for (const iface of project.getReflectionsByKind(ReflectionKind.Interface)) { + const decl = iface as DeclarationReflection; + const typekitTag = decl.comment?.blockTags.find((x) => x.tag === "@typekit"); + if (typekitTag) { + const name = renderDisplayParts(typekitTag.content).trim(); + entries.push([name, decl]); + } + } + // Kits are declared across many files via module augmentation, so reflection + // discovery order is not stable. Sort alphabetically for deterministic output. + entries.sort(([a], [b]) => a.localeCompare(b)); + for (const [name, decl] of entries) { + namespaces[name] = resolveTypekit(decl, [name]); + } + + return { namespaces }; +} + +function resolveTypekit(iface: DeclarationReflection, path: string[]): TypekitNamespace { + const typekit: TypekitNamespace = { + kind: "namespace", + name: path[0], + typeName: iface.name, + doc: iface.comment + ? toParagraphs(renderDisplayParts(iface.comment.summary)).join("\n\n") + : undefined, + entries: {}, + }; + + for (const member of [...(iface.children ?? [])].sort(byName)) { + if (member.kindOf(ReflectionKind.Accessor) && member.getSignature) { + // `get foo(): T` accessor. + typekit.entries[member.name] = { + kind: "getter", + name: member.name, + tsdoc: renderTsdoc(member.getSignature.comment ?? member.comment), + path: [...path, member.name], + parameters: [], + returnType: typeToString(member.getSignature.type), + }; + } else if (member.kindOf(ReflectionKind.Method) && member.signatures?.length) { + // Mirror the previous behavior where the last overload wins. + const signature = member.signatures[member.signatures.length - 1]; + typekit.entries[member.name] = createMethodDoc(member, signature, path); + } else if (member.kindOf(ReflectionKind.Property)) { + const type = member.type; + if (type?.type === "reference" && type.reflection?.kindOf(ReflectionKind.Interface)) { + // Property referencing another kit interface -> nested typekit. + typekit.entries[member.name] = resolveTypekit(type.reflection as DeclarationReflection, [ + ...path, + member.name, + ]); + } else if (type?.type === "reference" && type.name === "Diagnosable") { + typekit.entries[member.name] = createDiagnosableDoc(member, type, path); + } else if (type?.type === "reference") { + throw new Error( + `All typekit properties should be sub kits but got a reference to ${type.name} for ${path.join(".")}.${member.name}.`, + ); + } + // Properties with inline/anonymous types (e.g. function-typed helpers) are skipped, + // matching the previous api-extractor based behavior. + } + } + + return typekit; +} + +function createMethodDoc( + member: DeclarationReflection, + signature: SignatureReflection, + path: string[], +): TypekitFunctionDoc { + const parameters = signature.parameters ?? []; + return { + kind: "method", + name: member.name, + tsdoc: renderTsdoc( + signature.comment ?? member.comment, + parameters.map((param) => ({ name: param.name, comment: param.comment })), + ), + path: [...path, member.name], + parameters: parameters.map((param) => ({ + name: param.name, + optional: param.flags.isOptional, + type: typeToString(param.type), + })), + returnType: typeToString(signature.type), + }; +} + +function createDiagnosableDoc( + member: DeclarationReflection, + type: Extract, + path: string[], +): TypekitFunctionDoc { + const fnSignature = getDiagnosableFunctionSignature(type); + return { + kind: "diagnosable", + name: member.name, + tsdoc: renderTsdoc(member.comment), + path: [...path, member.name], + parameters: fnSignature + ? (fnSignature.parameters ?? []).map((param) => ({ + name: param.name, + optional: param.flags.isOptional, + type: typeToString(param.type), + })) + : [], + returnType: fnSignature ? typeToString(fnSignature.type) : "unknown", + }; +} + +/** + * A `Diagnosable` wraps a function type `T`. Extract that function signature. + */ +function getDiagnosableFunctionSignature( + type: Extract, +): SignatureReflection | undefined { + const arg = type.typeArguments?.[0]; + if (arg?.type === "reflection") { + return arg.declaration.signatures?.[0]; + } + return undefined; +} + +function typeToString(type: SomeType | undefined): string { + return type ? type.toString() : "unknown"; +} + +function byName(a: DeclarationReflection, b: DeclarationReflection): number { + return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; +} + +/** + * Resolve the source `.ts` entrypoint for the first `typekit` package export. + */ +function resolveTypekitSourceEntrypoint( + libraryPath: string, + pkgJson: PackageJson, +): string | undefined { + const exports = pkgJson.exports; + if (!exports) { + return undefined; + } + const typekitExport = Object.keys(exports).find((x) => x.includes("typekit")); + if (!typekitExport) { + return undefined; + } + const typesPath = resolveTypesPath((exports as Record)[typekitExport]); + if (!typesPath) { + return undefined; + } + // e.g. ./dist/src/typekit/index.d.ts -> ./src/typekit/index.ts + const sourceRelative = typesPath.replace(/^\.\/dist\//, "./").replace(/\.d\.ts$/, ".ts"); + return joinPaths(libraryPath, sourceRelative); +} + +function resolveTypesPath(entry: unknown): string | undefined { + if (typeof entry === "string") { + return entry.replace(/\.js$/, ".d.ts"); + } + if (entry && typeof entry === "object") { + const obj = entry as Record; + if (typeof obj.types === "string") { + return obj.types; + } + for (const condition of ["import", "default"]) { + const resolved = resolveTypesPath(obj[condition]); + if (resolved) { + return resolved; + } + } + } + return undefined; +} + +function renderDisplayParts(parts: readonly CommentDisplayPart[]): string { + return parts + .map((part) => { + if (part.kind === "inline-tag" && part.tag === "@link") { + return `{@link ${part.text}}`; + } + return part.text; + }) + .join(""); +} + +/** + * Collapse soft line breaks so each paragraph is a single line (matching the + * previous api-extractor tsdoc rendering). Paragraphs are separated by blank lines. + */ +function toParagraphs(text: string): string[] { + return text + .split(/\n[ \t]*\n/) + .map((paragraph) => + paragraph + .replace(/\s*\n\s*/g, " ") + .replace(/[ \t]+/g, " ") + .trim(), + ) + .filter((paragraph) => paragraph.length > 0); +} + +/** + * Render a TypeDoc comment into a tsdoc `/** ... */` block string. + * + * TypeDoc moves `@param` descriptions onto each parameter reflection, so for + * function signatures the parameters must be passed in to reconstruct them. + */ +export function renderTsdoc( + comment: Comment | undefined, + params: readonly { name: string; comment?: Comment }[] = [], +): string | undefined { + const blocks: string[][] = []; + + if (comment) { + for (const paragraph of toParagraphs(renderDisplayParts(comment.summary))) { + blocks.push([paragraph]); + } + } + + for (const param of params) { + const description = param.comment ? renderDisplayParts(param.comment.summary) : ""; + const paragraphs = toParagraphs(description); + if (paragraphs.length > 0) { + blocks.push([`@param ${param.name} - ${paragraphs.join(" ")}`]); + } + } + + for (const tag of comment?.blockTags ?? []) { + const paragraphs = toParagraphs(renderDisplayParts(tag.content).replace(/^-\s*/, "")); + switch (tag.tag) { + case "@param": + if (paragraphs.length === 0) break; + blocks.push([`@param ${tag.name ?? ""} - ${paragraphs.join(" ")}`]); + break; + case "@returns": + if (paragraphs.length === 0) break; + blocks.push([`@returns ${paragraphs[0] ?? ""}`]); + for (const extra of paragraphs.slice(1)) { + blocks.push([extra]); + } + break; + case "@remarks": + if (paragraphs.length === 0) break; + blocks.push(["@remarks"]); + for (const paragraph of paragraphs) { + blocks.push([paragraph]); + } + break; + default: + if (paragraphs.length === 0) break; + blocks.push([`${tag.tag} ${paragraphs[0] ?? ""}`.trimEnd()]); + for (const extra of paragraphs.slice(1)) { + blocks.push([extra]); + } + break; + } + } + + if (blocks.length === 0) { + return undefined; + } + const lines = blocks.flatMap((block, index) => (index === 0 ? block : ["", ...block])); + const body = lines.map((line) => (line ? ` * ${line}` : " *")).join("\n"); + return `/**\n${body}\n */`; +} diff --git a/packages/xml/api-extractor.json b/packages/xml/api-extractor.json deleted file mode 100644 index 2069b8ac37f..00000000000 --- a/packages/xml/api-extractor.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "extends": "../../api-extractor.base.json" -} diff --git a/packages/xml/package.json b/packages/xml/package.json index 37baeadb3bc..8683fffde6c 100644 --- a/packages/xml/package.json +++ b/packages/xml/package.json @@ -31,7 +31,7 @@ }, "scripts": { "clean": "rimraf ./dist ./temp", - "build": "pnpm gen-extern-signature && tsc -p tsconfig.build.json && pnpm lint-typespec-library && pnpm api-extractor", + "build": "pnpm gen-extern-signature && tsc -p tsconfig.build.json && pnpm lint-typespec-library && pnpm check-api-docs", "watch": "tsc -p tsconfig.build.json --watch", "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", @@ -42,7 +42,7 @@ "lint": "oxlint . --deny-warnings", "lint:fix": "oxlint . --fix", "regen-docs": "tspd doc . --enable-experimental --output-dir ../../website/src/content/docs/docs/libraries/xml/reference", - "api-extractor": "api-extractor run --local --verbose" + "check-api-docs": "tsx ../../eng/tsp-core/scripts/check-api-docs.ts ." }, "files": [ "lib/*.tsp", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18bcf63159a..40ea45340f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,18 +81,6 @@ catalogs: '@inquirer/prompts': specifier: ^8.4.1 version: 8.5.2 - '@microsoft/api-extractor': - specifier: ^7.58.1 - version: 7.58.9 - '@microsoft/api-extractor-model': - specifier: ^7.33.5 - version: 7.33.8 - '@microsoft/tsdoc': - specifier: ^0.16.0 - version: 0.16.0 - '@microsoft/tsdoc-config': - specifier: ^0.18.1 - version: 0.18.1 '@octokit/core': specifier: ^7.0.6 version: 7.0.6 @@ -518,9 +506,6 @@ importers: '@chronus/github-pr-commenter': specifier: 'catalog:' version: 1.0.6 - '@microsoft/api-extractor': - specifier: 'catalog:' - version: 7.58.9(@types/node@26.1.1) '@octokit/core': specifier: 'catalog:' version: 7.0.6 @@ -584,6 +569,9 @@ importers: turbo: specifier: 'catalog:' version: 2.9.14 + typedoc: + specifier: 'catalog:' + version: 0.28.20(typescript@6.0.3) typescript: specifier: 'catalog:' version: 6.0.3 @@ -2694,18 +2682,6 @@ importers: '@alloy-js/typescript': specifier: 'catalog:' version: 0.24.0 - '@microsoft/api-extractor': - specifier: 'catalog:' - version: 7.58.9(@types/node@26.1.1) - '@microsoft/api-extractor-model': - specifier: 'catalog:' - version: 7.33.8(@types/node@26.1.1) - '@microsoft/tsdoc': - specifier: 'catalog:' - version: 0.16.0 - '@microsoft/tsdoc-config': - specifier: 'catalog:' - version: 0.18.1 '@typespec/compiler': specifier: workspace:^ version: link:../compiler @@ -15052,6 +15028,7 @@ snapshots: '@rushstack/node-core-library': 5.23.1(@types/node@26.1.1) transitivePeerDependencies: - '@types/node' + optional: true '@microsoft/api-extractor@7.58.9(@types/node@26.1.1)': dependencies: @@ -15070,6 +15047,7 @@ snapshots: typescript: 5.9.3 transitivePeerDependencies: - '@types/node' + optional: true '@microsoft/applicationinsights-channel-js@3.4.3(tslib@2.8.1)': dependencies: @@ -15120,8 +15098,10 @@ snapshots: ajv: 8.18.0 jju: 1.4.0 resolve: 1.22.12 + optional: true - '@microsoft/tsdoc@0.16.0': {} + '@microsoft/tsdoc@0.16.0': + optional: true '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: @@ -15729,15 +15709,18 @@ snapshots: semver: 7.7.4 optionalDependencies: '@types/node': 26.1.1 + optional: true '@rushstack/problem-matcher@0.2.1(@types/node@26.1.1)': optionalDependencies: '@types/node': 26.1.1 + optional: true '@rushstack/rig-package@0.7.3': dependencies: jju: 1.4.0 resolve: 1.22.12 + optional: true '@rushstack/terminal@0.24.0(@types/node@26.1.1)': dependencies: @@ -15746,6 +15729,7 @@ snapshots: supports-color: 8.1.1 optionalDependencies: '@types/node': 26.1.1 + optional: true '@rushstack/ts-command-line@5.3.10(@types/node@26.1.1)': dependencies: @@ -15755,6 +15739,7 @@ snapshots: string-argv: 0.3.2 transitivePeerDependencies: - '@types/node' + optional: true '@scalar/helpers@0.9.0': {} @@ -16179,7 +16164,8 @@ snapshots: tslib: 2.8.1 optional: true - '@types/argparse@1.0.38': {} + '@types/argparse@1.0.38': + optional: true '@types/aria-query@5.0.4': {} @@ -17258,6 +17244,7 @@ snapshots: ajv-draft-04@1.0.0(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 + optional: true ajv-draft-04@1.0.0(ajv@8.20.0): optionalDependencies: @@ -17266,6 +17253,7 @@ snapshots: ajv-formats@3.0.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 + optional: true ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: @@ -17281,6 +17269,7 @@ snapshots: fast-uri: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + optional: true ajv@8.20.0: dependencies: @@ -18387,7 +18376,8 @@ snapshots: diff@8.0.4: {} - diff@9.0.0: {} + diff@9.0.0: + optional: true direction@2.0.1: {} @@ -19406,7 +19396,8 @@ snapshots: import-fresh@4.0.0: {} - import-lazy@4.0.0: {} + import-lazy@4.0.0: + optional: true import-meta-resolve@4.2.0: {} @@ -19589,7 +19580,8 @@ snapshots: editions: 6.22.0 textextensions: 6.11.0 - jju@1.4.0: {} + jju@1.4.0: + optional: true js-tokens@10.0.0: {} @@ -20533,6 +20525,7 @@ snapshots: minimatch@10.2.3: dependencies: brace-expansion: 5.0.7 + optional: true minimatch@10.2.5: dependencies: @@ -21833,7 +21826,8 @@ snapshots: semver@6.3.1: {} - semver@7.7.4: {} + semver@7.7.4: + optional: true semver@7.8.5: {} @@ -22141,7 +22135,8 @@ snapshots: - bare-abort-controller - react-native-b4a - string-argv@0.3.2: {} + string-argv@0.3.2: + optional: true string-width@4.2.3: dependencies: @@ -22233,6 +22228,7 @@ snapshots: supports-color@8.1.1: dependencies: has-flag: 4.0.0 + optional: true supports-hyperlinks@3.2.0: dependencies: @@ -22565,7 +22561,8 @@ snapshots: dependencies: semver: 7.8.5 - typescript@5.9.3: {} + typescript@5.9.3: + optional: true typescript@6.0.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f91c089fe70..e681caea793 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -45,10 +45,6 @@ catalog: "@fluentui/react-icons": ^2.0.323 "@fluentui/react-list": ^9.6.13 "@inquirer/prompts": ^8.4.1 - "@microsoft/api-extractor": ^7.58.1 - "@microsoft/api-extractor-model": ^7.33.5 - "@microsoft/tsdoc": ^0.16.0 - "@microsoft/tsdoc-config": ^0.18.1 "@octokit/core": ^7.0.6 "@octokit/plugin-paginate-graphql": ^6.0.0 "@octokit/plugin-rest-endpoint-methods": ^17.0.0 diff --git a/website/src/content/docs/docs/libraries/http/reference/typekits.mdx b/website/src/content/docs/docs/libraries/http/reference/typekits.mdx index dbb44a734a6..1c214509237 100644 --- a/website/src/content/docs/docs/libraries/http/reference/typekits.mdx +++ b/website/src/content/docs/docs/libraries/http/reference/typekits.mdx @@ -4,6 +4,94 @@ title: "[API] Typekits" import { Badge } from '@astrojs/starlight/components'; +## httpOperation + +Utilities for working with HTTP operations. + +### flattenResponses + +```ts +/** + * Get the responses for the given operation. This function will return an array of responses grouped by status code and content type. + * + * @param op - operation to extract the HttpResponse from + */ +$(program).httpOperation + .flattenResponses(op: HttpOperation): FlatHttpResponse[]; +``` + +### get + +```ts +/** + * Get the corresponding HTTP operation for the given TypeSpec operation. The same TypeSpec operation will always return the exact same HttpOperation object. + * + * @param op - The TypeSpec operation to get the HTTP operation metadata for. + */ +$(program).httpOperation.get(op: Operation): HttpOperation; +$(program).httpOperation.get + .withDiagnostics(op: Operation): [HttpOperation, readonly Diagnostic[]]; +``` + +### getReturnType + +```ts +/** + * Get the Http Return type for the given operation. This function will resolve the returnType based on the Http Operation. + * + * @param op - operation to get the return type for + */ +$(program).httpOperation.getReturnType( + op: HttpOperation, + options?: { includeErrors?: boolean } +): Type; +``` + +## httpPart + +Utilities for working with HTTP Parts. + +### get + +```ts +$(program).httpPart.get(type: Type): HttpPart | undefined; +``` + +### is + +```ts +/** + * Check if the model is a HTTP part. + * + * @param type - model to check + */ +$(program).httpPart.is(type: Type): boolean; +``` + +### unpack + +```ts +/** + * Unpacks the wrapped model from the HTTP part or the original model if not an HttpPart. + * + * @param type - HttpPart model to unpack + */ +$(program).httpPart.unpack(type: Type): Type; +``` + +## httpResponse + +Utilities for working with HTTP responses. + +### isErrorResponse + +```ts +/** + * Check if the response is an error response. + */ +$(program).httpResponse.isErrorResponse(response: FlatHttpResponse): boolean; +``` + ## model Utilities for working with Models in the context of Http. @@ -44,7 +132,7 @@ $(program).modelProperty * @param prop - a TypeSpec ModelProperty */ $(program).modelProperty - .getHttpParamOptions(prop: ModelProperty): HeaderFieldOptions | PathParameterOptions | QueryParameterOptions | undefined; + .getHttpParamOptions(prop: ModelProperty): HeaderFieldOptions | QueryParameterOptions | PathParameterOptions | undefined; ``` ### getHttpPathOptions @@ -114,93 +202,3 @@ $(program).modelProperty.isHttpPathParam(prop: ModelProperty): boolean; */ $(program).modelProperty.isHttpQueryParam(prop: ModelProperty): boolean; ``` - -## httpOperation - -Utilities for working with HTTP operations. - -### flattenResponses - -```ts -/** - * Get the responses for the given operation. This function will return an array of responses grouped by status code and content type. - * - * @param op - operation to extract the HttpResponse from - */ -$(program).httpOperation - .flattenResponses(op: HttpOperation): FlatHttpResponse[]; -``` - -### get - -```ts -/** - * Get the corresponding HTTP operation for the given TypeSpec operation. The same TypeSpec operation will always return the exact same HttpOperation object. - * - * @param op - The TypeSpec operation to get the HTTP operation metadata for. - */ -$(program).httpOperation.get(op: Operation): HttpOperation; -$(program).httpOperation.get - .withDiagnostics(op: Operation): [HttpOperation, readonly Diagnostic[]]; -``` - -### getReturnType - -```ts -/** - * Get the Http Return type for the given operation. This function will resolve the returnType based on the Http Operation. - * - * @param op - operation to get the return type for - */ -$(program).httpOperation.getReturnType( - op: HttpOperation, - options: { - includeErrors?: boolean; - } -): Type; -``` - -## httpPart - -Utilities for working with HTTP Parts. - -### get - -```ts -$(program).httpPart.get(type: Type): HttpPart | undefined; -``` - -### is - -```ts -/** - * Check if the model is a HTTP part. - * - * @param type - model to check - */ -$(program).httpPart.is(type: Type): boolean; -``` - -### unpack - -```ts -/** - * Unpacks the wrapped model from the HTTP part or the original model if not an HttpPart. - * - * @param type - HttpPart model to unpack - */ -$(program).httpPart.unpack(type: Type): Type; -``` - -## httpResponse - -Utilities for working with HTTP responses. - -### isErrorResponse - -```ts -/** - * Check if the response is an error response. - */ -$(program).httpResponse.isErrorResponse(response: FlatHttpResponse): boolean; -``` diff --git a/website/src/content/docs/docs/standard-library/reference/typekits.mdx b/website/src/content/docs/docs/standard-library/reference/typekits.mdx index 93bab0a07f6..02ddb53a9a3 100644 --- a/website/src/content/docs/docs/standard-library/reference/typekits.mdx +++ b/website/src/content/docs/docs/standard-library/reference/typekits.mdx @@ -281,9 +281,9 @@ Typekits for working with the top level entity. * @param diagnosticTarget - Target for the diagnostic */ $(program).entity - .isAssignableTo(source: Entity, target: Entity, diagnosticTarget?: Entity | Node): boolean; + .isAssignableTo(source: Entity, target: Entity, diagnosticTarget?: Node | Entity): boolean; $(program).entity.isAssignableTo - .withDiagnostics(source: Entity, target: Entity, diagnosticTarget?: Entity | Node): [boolean, readonly Diagnostic[]]; + .withDiagnostics(source: Entity, target: Entity, diagnosticTarget?: Node | Entity): [boolean, readonly Diagnostic[]]; ``` ### resolve @@ -294,9 +294,9 @@ $(program).entity.isAssignableTo * * Call `resolve.withDiagnostics("Type")` to get a tuple containing the resolved type and any diagnostics. */ -$(program).entity.resolve(): reference: stringEntity | undefined; +$(program).entity.resolve(reference: string): Entity | undefined; $(program).entity.resolve - .withDiagnostics(): [reference: stringEntity | undefined, readonly Diagnostic[]]; + .withDiagnostics(reference: string): [Entity | undefined, readonly Diagnostic[]]; ``` ## enum @@ -433,7 +433,7 @@ A Typekit for working with literal types(string, numeric, boolean). * @param value - The JavaScript value to turn into a TypeSpec literal type. */ $(program).literal - .create(value: string | number | boolean): StringLiteral | NumericLiteral | BooleanLiteral; + .create(value: string | number | boolean): BooleanLiteral | NumericLiteral | StringLiteral; ``` ### createBoolean @@ -478,7 +478,7 @@ $(program).literal.createString(value: string): StringLiteral; * @param type - The type to check. */ $(program).literal - .is(type: Entity): type is StringLiteral | NumericLiteral | BooleanLiteral; + .is(type: Entity): type is BooleanLiteral | NumericLiteral | StringLiteral; ``` ### isBoolean @@ -562,7 +562,7 @@ $(program).model.getDiscriminatedUnion /** * If the input is anonymous (or the provided filter removes properties) and there exists a named model with the same set of properties (ignoring filtered properties), then return that named model. Otherwise, return the input unchanged. * - * This can be used by emitters to find a better name for a set of properties after filtering. For example, given `{ @metadata prop: string} & SomeName`, and an emitter that wishes to discard properties marked with `@metadata`, the emitter can use this to recover that the best name for the remaining properties is `SomeName`. + * This can be used by emitters to find a better name for a set of properties after filtering. For example, given `{ @metadata prop: string} & SomeName`, and an emitter that wishes to discard properties marked with `@metadata`, the emitter can use this to recover that the best name for the remaining properties is `SomeName`. * * @param model - The input model * @@ -570,7 +570,7 @@ $(program).model.getDiscriminatedUnion */ $(program).model.getEffectiveModel( model: Model, - filter: (property: ModelProperty) => boolean + filter?: (property: ModelProperty) => boolean ): Model; ``` @@ -584,9 +584,7 @@ $(program).model.getEffectiveModel( */ $(program).model.getProperties( model: Model, - options: { - includeExtended?: boolean; - } + options?: { includeExtended?: boolean } ): RekeyableMap; ``` @@ -1362,7 +1360,7 @@ $(program).scalar.isUtcDateTime(type: Entity): type is Scalar; * * @param values - The tuple values, if any. */ -$(program).tuple.create(values: Type[]): Tuple; +$(program).tuple.create(values?: Type[]): Tuple; ``` ### is @@ -1414,9 +1412,7 @@ $(program).type.getDoc(type: Type): string | undefined; * Get the name of this type in the specified encoding. */ $(program).type.getEncodedName( - type: Type & { - name: string; - }, + type: Type & { name: string }, encoding: string ): string; ``` @@ -1429,7 +1425,7 @@ $(program).type.getEncodedName( * * @param type - The scalar to get the name of.z */ -$(program).type.getPlausibleName(type: Model | Union | Enum | Scalar): string; +$(program).type.getPlausibleName(type: Enum | Model | Scalar | Union): string; ``` ### getSummary @@ -1482,9 +1478,9 @@ $(program).type.is(entity: Entity): entity is Type; * @param diagnosticTarget - Target for the diagnostic */ $(program).type - .isAssignableTo(source: Type, target: Entity, diagnosticTarget?: Entity | Node): boolean; + .isAssignableTo(source: Type, target: Entity, diagnosticTarget?: Node | Entity): boolean; $(program).type.isAssignableTo - .withDiagnostics(source: Type, target: Entity, diagnosticTarget?: Entity | Node): [boolean, readonly Diagnostic[]]; + .withDiagnostics(source: Type, target: Entity, diagnosticTarget?: Node | Entity): [boolean, readonly Diagnostic[]]; ``` ### isError @@ -1558,8 +1554,6 @@ $(program).type.maxValue(type: Type): number | undefined; ```ts /** * Gets the maximum value this numeric type should be, exclusive of the given value. - * - * @param type - */ $(program).type.maxValueExclusive(type: Type): number | undefined; ``` @@ -1619,13 +1613,9 @@ $(program).type.minValueExclusive(type: Type): number | undefined; * Call `type.resolve.withDiagnostics("reference")` to get a tuple containing the resolved type and any diagnostics. */ $(program).type - .resolve((reference: string, kind?: KK extends Type["kind"] ? Extract : undefined); + .resolve(reference: string, kind?: K): K extends "Boolean" | "Decorator" | "FunctionType" | "Enum" | "EnumMember" | "FunctionParameter" | "Interface" | "Intrinsic" | "Model" | "ModelProperty" | "Namespace" | "Number" | "Operation" | "Scalar" | "ScalarConstructor" | "String" | "StringTemplate" | "StringTemplateSpan" | "TemplateParameter" | "TemplateParameterAccess" | "Tuple" | "Union" | "UnionVariant" ? Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract : undefined; $(program).type.resolve - .withDiagnostics((reference: string, kind?: KK extends Type["kind"] ? Extract : undefined), readonly Diagnostic[]]; + .withDiagnostics(reference: string, kind?: K): [K extends "Boolean" | "Decorator" | "FunctionType" | "Enum" | "EnumMember" | "FunctionParameter" | "Interface" | "Intrinsic" | "Model" | "ModelProperty" | "Namespace" | "Number" | "Operation" | "Scalar" | "ScalarConstructor" | "String" | "StringTemplate" | "StringTemplateSpan" | "TemplateParameter" | "TemplateParameterAccess" | "Tuple" | "Union" | "UnionVariant" ? Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract : undefined, readonly Diagnostic[]]; ``` ## union @@ -1638,11 +1628,7 @@ Utilities for working with unions. /** * Create an anonymous union type from an array of types. * - * @param children - The types to create a union from. - * - * Any API documentation will be rendered and preserved in the resulting union. - * - * No other decorators are copied from the enum to the union. + * @param children - The types to create a union from. Any API documentation will be rendered and preserved in the resulting union. No other decorators are copied from the enum to the union. */ $(program).union.create(children: Type[]): Union; ``` @@ -1653,15 +1639,7 @@ $(program).union.create(children: Type[]): Union; /** * Creates a union type from an enum. * - * @remarks - * - * @param type - The enum to create a union from. - * - * For member without an explicit value, the member name is used as the value. - * - * Any API documentation will be rendered and preserved in the resulting union. - * - * No other decorators are copied from the enum to the union. + * @param type - The enum to create a union from. For member without an explicit value, the member name is used as the value. Any API documentation will be rendered and preserved in the resulting union. No other decorators are copied from the enum to the union. */ $(program).union.createFromEnum(type: Enum): Union; ``` @@ -1958,11 +1936,7 @@ $(program).value.isString(type: Entity): type is StringValue; * Call `value.resolve.withDiagnostics("reference")` to get a tuple containing the resolved value and any diagnostics. */ $(program).value - .resolve((reference: string, kind?: KK extends Value["valueKind"] ? Extract : undefined); + .resolve(reference: string, kind?: K): K extends "NumericValue" | "ScalarValue" | "StringValue" | "BooleanValue" | "ObjectValue" | "ArrayValue" | "NullValue" | "EnumValue" | "Function" ? Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract, { valueKind: K }> : undefined; $(program).value.resolve - .withDiagnostics((reference: string, kind?: KK extends Value["valueKind"] ? Extract : undefined), readonly Diagnostic[]]; + .withDiagnostics(reference: string, kind?: K): [K extends "NumericValue" | "ScalarValue" | "StringValue" | "BooleanValue" | "ObjectValue" | "ArrayValue" | "NullValue" | "EnumValue" | "Function" ? Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract | Extract, { valueKind: K }> : undefined, readonly Diagnostic[]]; ```