From bf0bd1b9bec9abc54f880fde16222e14561c0705 Mon Sep 17 00:00:00 2001 From: Leo Ribeiro Date: Wed, 7 Feb 2024 14:18:16 -0500 Subject: [PATCH] Add typedoc reporter (#134) * add typedoc report * Add typedoc report * fix typedoc reporter only --- README.md | 6 +- badges/coverage.svg | 2 +- package-lock.json | 44 +++---- package.json | 2 +- src/docs-generator/typedoc.ts | 12 +- src/docs-report/index.ts | 3 + src/docs-report/interfaces.ts | 2 +- src/docs-report/typedoc.ts | 225 ++++++++++++++++++++++++++++++++++ 8 files changed, 265 insertions(+), 31 deletions(-) create mode 100644 src/docs-report/typedoc.ts diff --git a/README.md b/README.md index 5f2618f..48f0bd7 100644 --- a/README.md +++ b/README.md @@ -166,10 +166,10 @@ node scripts/main.js cd examples/foo && npm i && npm run build && cd ../.. export INPUT_ENTRY_POINTS=" - file: src/index.ts - docsReporter: api-extractor + docsReporter: typedoc docsGenerator: typedoc-markdown - file: examples/foo/index.ts - docsReporter: api-extractor + docsReporter: typedoc docsGenerator: typedoc-markdown " node scripts/main.js @@ -182,7 +182,7 @@ that you have in your local environment without needing to setup and wait for a real GH action execution. ```sh -docker build -f Dockerfile . --tag tbdocs-app:latest +docker build -f Dockerfile . --tag tbdocs:latest # now from the repo you want to analyze & generate docs # below is an example of running it from the root of tbdex-js repo diff --git a/badges/coverage.svg b/badges/coverage.svg index 11c80b3..40f1648 100644 --- a/badges/coverage.svg +++ b/badges/coverage.svg @@ -1 +1 @@ -Coverage: 36.63%Coverage36.63% \ No newline at end of file +Coverage: 32.86%Coverage32.86% \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b3e21a7..88bdc81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "simple-git": "^3.20.0", "ts-is-present": "^1.2.2", "tsconfck": "^2.1.2", - "typedoc": "^0.25.2", + "typedoc": "^0.25.7", "typedoc-plugin-markdown": "^3.16.0", "yaml": "^2.3.3" }, @@ -6158,9 +6158,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" }, "node_modules/jsonfile": { "version": "4.0.0", @@ -7603,9 +7603,9 @@ } }, "node_modules/shiki": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.4.tgz", - "integrity": "sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ==", + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", + "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", "dependencies": { "ansi-sequence-parser": "^1.1.0", "jsonc-parser": "^3.2.0", @@ -8192,14 +8192,14 @@ } }, "node_modules/typedoc": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.2.tgz", - "integrity": "sha512-286F7BeATBiWe/qC4PCOCKlSTwfnsLbC/4cZ68oGBbvAqb9vV33quEOXx7q176OXotD+JdEerdQ1OZGJ818lnA==", + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.7.tgz", + "integrity": "sha512-m6A6JjQRg39p2ZVRIN3NKXgrN8vzlHhOS+r9ymUYtcUP/TIQPvWSq7YgE5ZjASfv5Vd5BW5xrir6Gm2XNNcOow==", "dependencies": { "lunr": "^2.3.9", "marked": "^4.3.0", "minimatch": "^9.0.3", - "shiki": "^0.14.1" + "shiki": "^0.14.7" }, "bin": { "typedoc": "bin/typedoc" @@ -8208,7 +8208,7 @@ "node": ">= 16" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x" + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x" } }, "node_modules/typedoc-plugin-markdown": { @@ -13189,9 +13189,9 @@ } }, "jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" }, "jsonfile": { "version": "4.0.0", @@ -14224,9 +14224,9 @@ "dev": true }, "shiki": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.4.tgz", - "integrity": "sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ==", + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", + "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", "requires": { "ansi-sequence-parser": "^1.1.0", "jsonc-parser": "^3.2.0", @@ -14642,14 +14642,14 @@ } }, "typedoc": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.2.tgz", - "integrity": "sha512-286F7BeATBiWe/qC4PCOCKlSTwfnsLbC/4cZ68oGBbvAqb9vV33quEOXx7q176OXotD+JdEerdQ1OZGJ818lnA==", + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.7.tgz", + "integrity": "sha512-m6A6JjQRg39p2ZVRIN3NKXgrN8vzlHhOS+r9ymUYtcUP/TIQPvWSq7YgE5ZjASfv5Vd5BW5xrir6Gm2XNNcOow==", "requires": { "lunr": "^2.3.9", "marked": "^4.3.0", "minimatch": "^9.0.3", - "shiki": "^0.14.1" + "shiki": "^0.14.7" }, "dependencies": { "brace-expansion": { diff --git a/package.json b/package.json index 7ef7c8a..935b528 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "simple-git": "^3.20.0", "ts-is-present": "^1.2.2", "tsconfck": "^2.1.2", - "typedoc": "^0.25.2", + "typedoc": "^0.25.7", "typedoc-plugin-markdown": "^3.16.0", "yaml": "^2.3.3" }, diff --git a/src/docs-generator/typedoc.ts b/src/docs-generator/typedoc.ts index 65887ae..4200300 100644 --- a/src/docs-generator/typedoc.ts +++ b/src/docs-generator/typedoc.ts @@ -27,9 +27,15 @@ export const generateTypedoc = async ( // Set project path if not set before by the doc-reporter if (!entryPoint.projectPath) { - const entryPointDir = path.dirname(entryPointFile) - const packageJsonPath = lookupFile(entryPointDir, 'package.json') - entryPoint.projectPath = packageJsonPath + const entryPointFileFullPath = path.dirname( + path.join(process.cwd(), entryPointFile) + ) + const packageJsonFullPath = lookupFile( + 'package.json', + entryPointFileFullPath + ) + const projectPath = path.dirname(packageJsonFullPath) + entryPoint.projectPath = projectPath } const { tsconfigFile } = await loadTsconfigProps(entryPoint.projectPath) diff --git a/src/docs-report/index.ts b/src/docs-report/index.ts index 6fbe48e..b7dc0ed 100644 --- a/src/docs-report/index.ts +++ b/src/docs-report/index.ts @@ -1,6 +1,7 @@ import { FilesDiffsMap, isSourceInChangedScope } from '../utils' import { EntryPoint } from '../interfaces' import { generateApiExtractorReport } from './api-extractor' +import { generateTypedocReport } from './typedoc' import { DocsReport } from './interfaces' export * from './report-markdown' @@ -26,6 +27,8 @@ const generateReport = async ( ignoreMessages?: string[] ): Promise => { switch (entryPoint.docsReporter) { + case 'typedoc': + return generateTypedocReport(entryPoint, ignoreMessages) case 'api-extractor': return generateApiExtractorReport(entryPoint, ignoreMessages) default: diff --git a/src/docs-report/interfaces.ts b/src/docs-report/interfaces.ts index 8d1e161..570f3d0 100644 --- a/src/docs-report/interfaces.ts +++ b/src/docs-report/interfaces.ts @@ -11,7 +11,7 @@ * * @public **/ -export type DocsReporterType = 'api-extractor' +export type DocsReporterType = 'api-extractor' | 'typedoc' /** * Object containing the result of running the docs reporter. diff --git a/src/docs-report/typedoc.ts b/src/docs-report/typedoc.ts new file mode 100644 index 0000000..5223cfc --- /dev/null +++ b/src/docs-report/typedoc.ts @@ -0,0 +1,225 @@ +import path from 'path' +import ts from 'typescript' + +import { DocsReport, MessageLevel } from './interfaces' +import { loadTsconfigProps, lookupFile } from '../utils' +import { EntryPoint } from '../interfaces' +import { + Application, + LogLevel, + Logger, + MinimalSourceFile, + TypeDocOptions, + ValidationOptions +} from 'typedoc' + +export const generateTypedocReport = async ( + entryPoint: EntryPoint, + ignoreMessages?: string[] +): Promise => { + console.info('ignoreMessages', ignoreMessages) + const entryPointFile = entryPoint.file + const typedocValidationConfig = initializeConfig(entryPointFile) + + const entryPointFileFullPath = path.dirname( + path.join(process.cwd(), entryPointFile) + ) + const packageJsonFullPath = lookupFile('package.json', entryPointFileFullPath) + const projectPath = path.dirname(packageJsonFullPath) + + entryPoint.projectPath = projectPath + + const { tsconfigFile } = await loadTsconfigProps(entryPoint.projectPath) + + const generatorConfig: Partial = { + tsconfig: tsconfigFile, + entryPoints: [entryPointFile], + validation: typedocValidationConfig + // skipErrorChecking: true, + // disableSources: true, + // readme: entryPoint.readmeFile || 'none', + // includeVersion: true + } + + console.info('>>> Typedoc Report entryPoint', entryPointFile, generatorConfig) + const generatorApp = await Application.bootstrapWithPlugins(generatorConfig) + + const projectReflection = await generatorApp.convert() + if (!projectReflection) { + throw new Error('Typedoc Report: Failed to generate project reflection') + } + + const logger = new TypedocReportLogger() + generatorApp.logger = logger + + generatorApp.validate(projectReflection) + return logger.getReport() +} + +const initializeConfig = (entryPointFile: string): ValidationOptions => { + console.info('>>> Typedoc Report initializeConfig', entryPointFile) + // TODO: allow dynamic config input from tbdocs action inputs + return { + notExported: true, + invalidLink: true, + notDocumented: true + } +} + +class TypedocReportLogger extends Logger { + private report: DocsReport = { + reporter: 'typedoc', + errorsCount: 0, + warningsCount: 0, + messages: [] + } + + getReport(): DocsReport { + return this.report + } + + addContext( + message: string, + level: LogLevel, + ...args: [ts.Node?] | [number, MinimalSourceFile] + ): string { + const messageLevel = { + [LogLevel.Error]: 'error', + [LogLevel.Warn]: 'warning', + [LogLevel.Info]: 'info', + [LogLevel.Verbose]: 'verbose', + [LogLevel.None]: 'none' + }[level] as MessageLevel + + let pos + let file + + console.info('typedoc-context args >>>', { message, level, args }) + if (typeof args[0] == 'undefined') { + pos = 0 + file = undefined + } else if (typeof args[0] !== 'number') { + pos = args[0].getStart(args[0].getSourceFile(), false) + file = args[0].getSourceFile() + } else { + pos = args[0] + file = args[1] + } + + let sourceFilePath + let sourceFileLine + let sourceFileColumn + let context + let text = message + if (file) { + const { line, character } = file.getLineAndCharacterOfPosition(pos) + sourceFilePath = file.fileName + sourceFileLine = line + sourceFileColumn = character + + const location = `${sourceFilePath}:${line + 1}:${character}` + + const start = file.text.lastIndexOf('\n', pos) + 1 + let end = file.text.indexOf('\n', start) + if (end === -1) end = file.text.length + + const prefix = `${location} - [${level}]` + + context = `${line + 1} ${file.text.substring(start, end)}` + text = `${prefix} ${message}\n\n${context}\n` + + sourceFilePath + } else { + const [rawMessage, fileName] = parseMessageFileLocation(message) + if (rawMessage && fileName) { + sourceFilePath = fileName + text = rawMessage + } + } + + this.reportMessage( + messageLevel, + text, + sourceFilePath, + sourceFileLine, + sourceFileColumn, + context + ) + + return text + } + + private reportMessage( + messageLevel: MessageLevel, + text: string, + sourceFilePath?: string, + sourceFileLine?: number, + sourceFileColumn?: number, + context?: string + ): void { + // count errors and warnings + switch (messageLevel) { + case 'error': + this.report.errorsCount += 1 + break + case 'warning': + this.report.warningsCount += 1 + break + case 'info': + break + default: + return + } + + this.report.messages.push({ + level: messageLevel, + category: 'extractor', + messageId: getTypedocMessageId(text), + text, + sourceFilePath, + sourceFileLine, + sourceFileColumn, + context + }) + } +} + +const getTypedocMessageId = (text: string): string => { + // exports validations + if (text.includes('missing export')) { + return 'typedoc:missing-export' + } else if (text.includes('intentionally not exported')) { + return 'typedoc:unintentional-export' + } else if (text.includes('not included in the documentation')) { + return 'typedoc:missing-reference' + + // links validations + } else if (text.includes('resolve link')) { + return 'typedoc:invalid-link' + + // docs validation + } else if (text.includes('not have any documentation')) { + return 'typedoc:missing-docs' + + // unknown + } else { + return 'typedoc:generic' + } +} + +const parseMessageFileLocation = (text: string): [string?, string?] => { + // Regular expression to capture the message and file path + const regex = /^(.+?), defined in (.*), (.+?)$/ + const match = text.match(regex) + + if (match) { + // Extracted parts from the log + const message = `${match[1]} ${match[3]}` + const file = match[2] + + // Creating the object with the extracted information + return [message, file] + } + + return [undefined, undefined] +}