diff --git a/.changeset/six-pugs-hug.md b/.changeset/six-pugs-hug.md new file mode 100644 index 0000000000..3918942ad3 --- /dev/null +++ b/.changeset/six-pugs-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': minor +--- + +Adds a new experimental hidden command `package-docs` for generating API documentation. This is currently only intended for use in the Backstage main repository. diff --git a/.gitignore b/.gitignore index 865918c1f7..6627583fce 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,8 @@ knip.json # Schemathesis temporary files .hypothesis/ .cassettes/ + +# Typedocs temporary files +type-docs +docs.json +tsconfig.typedoc.tmp.json \ No newline at end of file diff --git a/package.json b/package.json index d7c1307d69..7e93c4b58c 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "shx": "^0.3.2", "sloc": "^0.3.1", "sort-package-json": "^2.8.0", + "typedoc": "^0.27.6", "typescript": "~5.2.0" }, "packageManager": "yarn@3.8.1", diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index b4dde33cb8..b50090dd6e 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -89,7 +89,8 @@ "@backstage/types": "workspace:^", "@types/is-glob": "^4.0.2", "@types/node": "^20.16.0", - "@types/prettier": "^2.0.0" + "@types/prettier": "^2.0.0", + "typedoc": "^0.27.6" }, "peerDependencies": { "@microsoft/api-extractor-model": "*", @@ -97,11 +98,15 @@ "@microsoft/tsdoc-config": "*", "@useoptic/optic": "^1.0.0", "prettier": "^2.8.1", + "typedoc": "^0.27.0", "typescript": "> 3.0.0" }, "peerDependenciesMeta": { "prettier": { "optional": true + }, + "typedoc": { + "optional": true } } } diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index c298384375..b737d918bd 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -262,6 +262,11 @@ export function registerCommands(program: Command) { lazy(() => import('./knip-reports/knip-reports'), 'buildKnipReports'), ); + program + .command('package-docs [paths...]', { hidden: true }) + .description('EXPERIMENTAL: Generate package documentation') + .action(lazy(() => import('./package-docs/command'), 'default')); + registerPackageCommand(program); registerRepoCommand(program); registerLintCommand(program); diff --git a/packages/repo-tools/src/commands/package-docs/command.ts b/packages/repo-tools/src/commands/package-docs/command.ts new file mode 100644 index 0000000000..8c30b06145 --- /dev/null +++ b/packages/repo-tools/src/commands/package-docs/command.ts @@ -0,0 +1,181 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { paths as cliPaths, resolvePackagePaths } from '../../lib/paths'; +import { createTemporaryTsConfig } from './utils'; +import { mkdir, readFile, writeFile } from 'fs/promises'; +import pLimit from 'p-limit'; + +const limit = pLimit(8); + +const execAsync = promisify(exec); + +const EXCLUDE = [ + 'packages/app', + 'packages/app-next', + 'packages/app-next-example-plugin', + 'packages/cli', + 'packages/cli-common', + 'packages/cli-node', + 'packages/e2e-test', + 'packages/e2e-test-utils', + 'packages/opaque-internal', + 'packages/techdocs-cli', + 'packages/techdocs-cli-embedded-app', + 'packages/yarn-plugin', + 'packages/backend', +]; + +const HIGHLIGHT_LANGUAGES = [ + 'ts', + 'tsx', + 'yaml', + 'bash', + 'sh', + 'shell', + 'yml', + 'jsx', + 'diff', + 'js', + 'json', +]; + +function getExports(packageJson: any) { + if (packageJson.exports) { + return Object.values(packageJson.exports).filter( + (e: any) => !(e as string).endsWith('package.json'), + ); + } + return [packageJson.main]; +} + +async function generateDocJson(pkg: string) { + const temporaryTsConfigPath: string = await createTemporaryTsConfig(pkg); + + const packageJson = JSON.parse( + await readFile(cliPaths.resolveTargetRoot(pkg, 'package.json'), 'utf-8'), + ); + + const exports = getExports(packageJson); + if ( + !exports.length || + !exports.some(e => e.startsWith('src') || e.startsWith('./src')) + ) { + return; + } + + try { + await mkdir(cliPaths.resolveTargetRoot(`dist-types`, pkg), { + recursive: true, + }); + + const { stdout, stderr } = await execAsync( + [ + cliPaths.resolveTargetRoot('node_modules/.bin/typedoc'), + '--json', + cliPaths.resolveTargetRoot(`dist-types`, pkg, 'docs.json'), + '--tsconfig', + temporaryTsConfigPath, + '--basePath', + cliPaths.targetRoot, + '--skipErrorChecking', + ...(getExports(packageJson).flatMap(e => [ + '--entryPoints', + e, + ]) as string[]), + ].join(' '), + { + cwd: pkg, + env: { ...process.env, NODE_OPTIONS: '--max-old-space-size=12288' }, + }, + ); + console.log(`### Processed ${pkg}`); + console.log(stdout); + console.error(stderr); + } catch (e) { + console.error('Failed to generate docs for', pkg); + console.error(e); + } +} + +export default async function packageDocs(paths: string[] = [], opts: any) { + console.warn('!!! This is an experimental command !!!'); + const selectedPackageDirs = await resolvePackagePaths({ + paths, + include: opts.include, + exclude: opts.exclude, + }); + + console.log(`### Generating docs.`); + await Promise.all( + selectedPackageDirs.map(pkg => + limit(async () => { + if (EXCLUDE.includes(pkg)) { + return; + } + console.log(`### Processing ${pkg}`); + await generateDocJson(pkg); + }), + ), + ); + + const generatedPackageDirs = []; + for (const pkg of selectedPackageDirs) { + try { + const docsJsonPath = cliPaths.resolveTargetRoot( + `dist-types/${pkg}/docs.json`, + ); + const docsJson = JSON.parse(await readFile(docsJsonPath, 'utf-8')); + const index = docsJson.children?.find((child: any) => + child.sources.some((e: any) => e.fileName.endsWith('src/index.ts')), + ); + + if (index) { + index.name = 'index'; + } + await writeFile(docsJsonPath, JSON.stringify(docsJson, null, 2)); + generatedPackageDirs.push(pkg); + } catch (e) { + if (e.code === 'ENOENT') { + console.log('No docs.json found for', pkg); + } else { + throw e; + } + } + } + + const { stdout, stderr } = await execAsync( + [ + cliPaths.resolveTargetRoot('node_modules/.bin/typedoc'), + '--entryPointStrategy', + 'merge', + ...generatedPackageDirs.flatMap(pkg => [ + '--entryPoints', + `dist-types/${pkg}/docs.json`, + ]), + ...HIGHLIGHT_LANGUAGES.flatMap(e => ['--highlightLanguages', e]), + '--out', + cliPaths.resolveTargetRoot('type-docs'), + ].join(' '), + { + cwd: cliPaths.targetRoot, + }, + ); + + console.log(stdout); + console.error(stderr); +} diff --git a/packages/repo-tools/src/commands/package-docs/utils.ts b/packages/repo-tools/src/commands/package-docs/utils.ts new file mode 100644 index 0000000000..2cb95443b6 --- /dev/null +++ b/packages/repo-tools/src/commands/package-docs/utils.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { paths as cliPaths } from '../../lib/paths'; + +export async function createTemporaryTsConfig(dir: string) { + const path = cliPaths.resolveOwnRoot(dir, 'tsconfig.typedoc.tmp.json'); + + process.once('exit', () => { + fs.removeSync(path); + }); + + let assetTypeFile: string[] = []; + + try { + assetTypeFile = [ + require.resolve('@backstage/cli/asset-types/asset-types.d.ts'), + ]; + } catch { + /** ignore */ + } + + await fs.writeJson(path, { + extends: '../../tsconfig.json', + include: [...assetTypeFile, 'src'], + exclude: [], + }); + + return path; +} diff --git a/yarn.lock b/yarn.lock index ad2aa54234..2bab06fc39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8748,6 +8748,7 @@ __metadata: portfinder: ^1.0.32 tar: ^6.1.12 ts-morph: ^24.0.0 + typedoc: ^0.27.6 yaml-diff-patch: ^2.0.0 peerDependencies: "@microsoft/api-extractor-model": "*" @@ -8755,10 +8756,13 @@ __metadata: "@microsoft/tsdoc-config": "*" "@useoptic/optic": ^1.0.0 prettier: ^2.8.1 + typedoc: ^0.27.0 typescript: "> 3.0.0" peerDependenciesMeta: prettier: optional: true + typedoc: + optional: true bin: backstage-repo-tools: bin/backstage-repo-tools languageName: unknown @@ -10159,6 +10163,17 @@ __metadata: languageName: node linkType: hard +"@gerrit0/mini-shiki@npm:^1.24.0": + version: 1.27.2 + resolution: "@gerrit0/mini-shiki@npm:1.27.2" + dependencies: + "@shikijs/engine-oniguruma": ^1.27.2 + "@shikijs/types": ^1.27.2 + "@shikijs/vscode-textmate": ^10.0.1 + checksum: aa4a0def0c7c73f2ef49b53f4dab5bf95f8f3b3d2c895baf384d338856a082a6a0487bc87c28d550b7f52ed71cbe98dae7754a5397be52df49a18e75cdd7d087 + languageName: node + linkType: hard + "@gitbeaker/core@npm:^35.8.1": version: 35.8.1 resolution: "@gitbeaker/core@npm:35.8.1" @@ -16591,6 +16606,33 @@ __metadata: languageName: node linkType: hard +"@shikijs/engine-oniguruma@npm:^1.27.2": + version: 1.29.2 + resolution: "@shikijs/engine-oniguruma@npm:1.29.2" + dependencies: + "@shikijs/types": 1.29.2 + "@shikijs/vscode-textmate": ^10.0.1 + checksum: 8713ada50e8875d22d928bd605d509a2c7d5e8c2c8a67b215b169f999457123082a02000182b37b9621903577dae5ac8067c614037fbf0aeb5b6dc2c195e58a2 + languageName: node + linkType: hard + +"@shikijs/types@npm:1.29.2, @shikijs/types@npm:^1.27.2": + version: 1.29.2 + resolution: "@shikijs/types@npm:1.29.2" + dependencies: + "@shikijs/vscode-textmate": ^10.0.1 + "@types/hast": ^3.0.4 + checksum: 3aeb2933b5ceda8afe6e4be624847de5fab392085ddf77fb785cf33014120d1afd6825e666d58895e4c489981196abc161c8a4d2e41f7da33d8f5e83b58cc606 + languageName: node + linkType: hard + +"@shikijs/vscode-textmate@npm:^10.0.1": + version: 10.0.1 + resolution: "@shikijs/vscode-textmate@npm:10.0.1" + checksum: c5a8490417b9439b055844c6c09c3435fc435b1fc3923eb28f05ee346fd68e69df2d93cdaab319a51193970558ff1bf49c5ab047c9ed4fd86c3f9d062457a565 + languageName: node + linkType: hard + "@short.io/opensearch-mock@npm:^0.4.0": version: 0.4.0 resolution: "@short.io/opensearch-mock@npm:0.4.0" @@ -19806,6 +19848,15 @@ __metadata: languageName: node linkType: hard +"@types/hast@npm:^3.0.4": + version: 3.0.4 + resolution: "@types/hast@npm:3.0.4" + dependencies: + "@types/unist": "*" + checksum: 7a973e8d16fcdf3936090fa2280f408fb2b6a4f13b42edeb5fbd614efe042b82eac68e298e556d50f6b4ad585a3a93c353e9c826feccdc77af59de8dd400d044 + languageName: node + linkType: hard + "@types/highlightjs@npm:^10.1.0": version: 10.1.0 resolution: "@types/highlightjs@npm:10.1.0" @@ -42842,6 +42893,7 @@ __metadata: shx: ^0.3.2 sloc: ^0.3.1 sort-package-json: ^2.8.0 + typedoc: ^0.27.6 typescript: ~5.2.0 languageName: unknown linkType: soft @@ -46054,6 +46106,23 @@ __metadata: languageName: node linkType: hard +"typedoc@npm:^0.27.6": + version: 0.27.6 + resolution: "typedoc@npm:0.27.6" + dependencies: + "@gerrit0/mini-shiki": ^1.24.0 + lunr: ^2.3.9 + markdown-it: ^14.1.0 + minimatch: ^9.0.5 + yaml: ^2.6.1 + peerDependencies: + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x + bin: + typedoc: bin/typedoc + checksum: 1a8ac5dd636406fa0bcd4a1e9801d72896256c531c09a56207f991b62b7d4ceb336050a8933a3aba0239da6df5da3436e51c86776043501b6e9483aa0b31e69f + languageName: node + linkType: hard + "types-ramda@npm:^0.30.0": version: 0.30.0 resolution: "types-ramda@npm:0.30.0" @@ -47982,7 +48051,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3, yaml@npm:^2.3.4, yaml@npm:^2.7.0": +"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.1.1, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.3.2, yaml@npm:^2.3.3, yaml@npm:^2.3.4, yaml@npm:^2.6.1, yaml@npm:^2.7.0": version: 2.7.0 resolution: "yaml@npm:2.7.0" bin: