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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
55 changes: 0 additions & 55 deletions api-extractor.base.json

This file was deleted.

161 changes: 161 additions & 0 deletions eng/tsp-core/scripts/check-api-docs.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
}

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<string, unknown>;
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<string>();
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);
});
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand All @@ -66,6 +65,7 @@
"rimraf": "catalog:",
"tsx": "catalog:",
"turbo": "catalog:",
"typedoc": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:",
"yaml": "catalog:"
Expand Down
4 changes: 0 additions & 4 deletions packages/asset-emitter/api-extractor.json

This file was deleted.

2 changes: 1 addition & 1 deletion packages/asset-emitter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 0 additions & 4 deletions packages/compiler/api-extractor.json

This file was deleted.

2 changes: 1 addition & 1 deletion packages/compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 0 additions & 4 deletions packages/graphql/api-extractor.json

This file was deleted.

4 changes: 0 additions & 4 deletions packages/http/api-extractor.json

This file was deleted.

2 changes: 1 addition & 1 deletion packages/http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 0 additions & 4 deletions packages/json-schema/api-extractor.json

This file was deleted.

4 changes: 2 additions & 2 deletions packages/json-schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 0 additions & 4 deletions packages/openapi/api-extractor.json

This file was deleted.

4 changes: 2 additions & 2 deletions packages/openapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 0 additions & 4 deletions packages/openapi3/api-extractor.json

This file was deleted.

2 changes: 1 addition & 1 deletion packages/openapi3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 0 additions & 4 deletions packages/tspd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
Loading
Loading