UNPKG

typedoc

Version:

Create api documentation for TypeScript projects.

314 lines (313 loc) 13.2 kB
import { join, relative, resolve } from "path"; import ts from "typescript"; import * as FS from "fs"; import { expandPackages } from "./package-manifest.js"; import { createMinimatch, matchesAny, nicePath, normalizePath, } from "./paths.js"; import { deriveRootDir, discoverPackageJson, getCommonDirectory, glob, inferPackageEntryPointPaths, isDir, } from "./fs.js"; import { assertNever } from "./general.js"; /** * Defines how entry points are interpreted. * @enum */ export const EntryPointStrategy = { /** * The default behavior in v0.22+, expects all provided entry points as being part of a single program. * Any directories included in the entry point list will result in `dir/index.([cm][tj]s|[tj]sx?)` being used. */ Resolve: "resolve", /** * The default behavior in v0.21 and earlier. Behaves like the resolve behavior, but will recursively * expand directories into an entry point for each file within the directory. */ Expand: "expand", /** * Run TypeDoc in each directory passed as an entry point. Once all directories have been converted, * use the merge option to produce final output. */ Packages: "packages", /** * Merges multiple previously generated output from TypeDoc's --json output together into a single project. */ Merge: "merge", }; export function inferEntryPoints(logger, options) { const packageJson = discoverPackageJson(options.packageDir ?? process.cwd()); if (!packageJson) { logger.warn(logger.i18n.no_entry_points_provided()); return []; } const pathEntries = inferPackageEntryPointPaths(packageJson.file); const entryPoints = []; const programs = getEntryPrograms(pathEntries.map((p) => p[1]), logger, options); // See also: addInferredDeclarationMapPaths in ReflectionSymbolId const jsToTsSource = new Map(); for (const program of programs) { const opts = program.getCompilerOptions(); const rootDir = opts.rootDir || getCommonDirectory(program.getRootFileNames()); const outDir = opts.outDir || rootDir; for (const tsFile of program.getRootFileNames()) { const jsFile = normalizePath(resolve(outDir, relative(rootDir, tsFile)).replace(/\.([cm]?)[tj]sx?$/, ".$1js")); jsToTsSource.set(jsFile, tsFile); } } for (const [name, path] of pathEntries) { // Strip leading ./ from the display name const displayName = name.replace(/^\.\/?/, ""); const targetPath = jsToTsSource.get(path) || path; const program = programs.find((p) => p.getSourceFile(targetPath)); if (program) { entryPoints.push({ displayName, program, sourceFile: program.getSourceFile(targetPath), }); } else if (/\.[cm]?js$/.test(path)) { logger.warn(logger.i18n.failed_to_resolve_0_to_ts_path(nicePath(path))); } } if (entryPoints.length === 0) { logger.warn(logger.i18n.no_entry_points_provided()); return []; } return entryPoints; } export function getEntryPoints(logger, options) { if (!options.isSet("entryPoints")) { logger.warn(logger.i18n.no_entry_points_provided()); return []; } const entryPoints = options.getValue("entryPoints"); const exclude = options.getValue("exclude"); // May be set explicitly to be an empty array to only include a readme for a package // See #2264 if (entryPoints.length === 0) { return []; } let result; const strategy = options.getValue("entryPointStrategy"); switch (strategy) { case EntryPointStrategy.Resolve: result = getEntryPointsForPaths(logger, expandGlobs(entryPoints, exclude, logger), options); break; case EntryPointStrategy.Expand: result = getExpandedEntryPointsForPaths(logger, expandGlobs(entryPoints, exclude, logger), options); break; case EntryPointStrategy.Merge: case EntryPointStrategy.Packages: // Doesn't really have entry points in the traditional way of how TypeDoc has dealt with them. return []; default: assertNever(strategy); } if (result.length === 0) { logger.error(logger.i18n.unable_to_find_any_entry_points()); return; } return result; } /** * Document entry points are markdown documents that the user has requested we include in the project with * an option rather than a `@document` tag. * * @returns A list of `.md` files to include in the documentation as documents. */ export function getDocumentEntryPoints(logger, options) { const docGlobs = options.getValue("projectDocuments"); if (docGlobs.length === 0) { return []; } const docPaths = expandGlobs(docGlobs, [], logger); // We might want to expand this in the future, there are quite a lot of extensions // that have at some point or another been used for markdown: https://superuser.com/a/285878 const supportedFileRegex = /\.(md|markdown)$/; const expanded = expandInputFiles(logger, docPaths, options, supportedFileRegex); const baseDir = options.getValue("basePath") || deriveRootDir(expanded); return expanded.map((path) => { return { displayName: relative(baseDir, path).replace(/\.[^.]+$/, ""), path, }; }); } export function getWatchEntryPoints(logger, options, program) { let result; const entryPoints = options.getValue("entryPoints"); const exclude = options.getValue("exclude"); const strategy = options.getValue("entryPointStrategy"); switch (strategy) { case EntryPointStrategy.Resolve: result = getEntryPointsForPaths(logger, expandGlobs(entryPoints, exclude, logger), options, [program]); break; case EntryPointStrategy.Expand: result = getExpandedEntryPointsForPaths(logger, expandGlobs(entryPoints, exclude, logger), options, [program]); break; case EntryPointStrategy.Packages: logger.error(logger.i18n.watch_does_not_support_packages_mode()); break; case EntryPointStrategy.Merge: logger.error(logger.i18n.watch_does_not_support_merge_mode()); break; default: assertNever(strategy); } if (result && result.length === 0) { logger.error(logger.i18n.unable_to_find_any_entry_points()); return; } return result; } export function getPackageDirectories(logger, options, packageGlobPaths) { const exclude = createMinimatch(options.getValue("exclude")); const rootDir = deriveRootDir(packageGlobPaths); // packages arguments are workspace tree roots, or glob patterns // This expands them to leave only leaf packages return expandPackages(logger, rootDir, packageGlobPaths, exclude); } function getModuleName(fileName, baseDir) { return normalizePath(relative(baseDir, fileName)).replace(/(\/index)?(\.d)?\.([cm][tj]s|[tj]sx?)$/, ""); } /** * Converts a list of file-oriented paths in to DocumentationEntryPoints for conversion. * This is in contrast with the package-oriented `getEntryPointsForPackages` */ function getEntryPointsForPaths(logger, inputFiles, options, programs = getEntryPrograms(inputFiles, logger, options)) { const baseDir = options.getValue("basePath") || deriveRootDir(inputFiles); const entryPoints = []; let expandSuggestion = true; entryLoop: for (const fileOrDir of inputFiles.map(normalizePath)) { const toCheck = [fileOrDir]; if (!/\.([cm][tj]s|[tj]sx?)$/.test(fileOrDir)) { toCheck.push(`${fileOrDir}/index.ts`, `${fileOrDir}/index.cts`, `${fileOrDir}/index.mts`, `${fileOrDir}/index.tsx`, `${fileOrDir}/index.js`, `${fileOrDir}/index.cjs`, `${fileOrDir}/index.mjs`, `${fileOrDir}/index.jsx`); } for (const program of programs) { for (const check of toCheck) { const sourceFile = program.getSourceFile(check); if (sourceFile) { entryPoints.push({ displayName: getModuleName(resolve(check), baseDir), sourceFile, program, }); continue entryLoop; } } } logger.warn(logger.i18n.entry_point_0_not_in_program(nicePath(fileOrDir))); if (expandSuggestion && isDir(fileOrDir)) { expandSuggestion = false; logger.info(logger.i18n.use_expand_or_glob_for_files_in_dir()); } } return entryPoints; } export function getExpandedEntryPointsForPaths(logger, inputFiles, options, programs = getEntryPrograms(inputFiles, logger, options)) { const compilerOptions = options.getCompilerOptions(); const supportedFileRegex = compilerOptions.allowJs || compilerOptions.checkJs ? /\.([cm][tj]s|[tj]sx?)$/ : /\.([cm]ts|tsx?)$/; return getEntryPointsForPaths(logger, expandInputFiles(logger, inputFiles, options, supportedFileRegex), options, programs); } function expandGlobs(inputFiles, exclude, logger) { const excludePatterns = createMinimatch(exclude); const base = deriveRootDir(inputFiles); const result = inputFiles.flatMap((entry) => { const result = glob(entry, base, { includeDirectories: true, followSymlinks: true, }); const filtered = result.filter((file) => file === entry || !matchesAny(excludePatterns, file)); if (result.length === 0) { logger.warn(logger.i18n.glob_0_did_not_match_any_files(nicePath(entry))); } else if (filtered.length === 0) { logger.warn(logger.i18n.entry_point_0_did_not_match_any_files_after_exclude(nicePath(entry))); } else if (filtered.length !== 1) { logger.verbose(`Expanded ${nicePath(entry)} to:\n\t${filtered .map(nicePath) .join("\n\t")}`); } return filtered; }); return result; } function getEntryPrograms(inputFiles, logger, options) { const noTsConfigFound = options.getFileNames().length === 0 && options.getProjectReferences().length === 0; const rootProgram = noTsConfigFound ? ts.createProgram({ rootNames: inputFiles, options: options.getCompilerOptions(), }) : ts.createProgram({ rootNames: options.getFileNames(), options: options.getCompilerOptions(), projectReferences: options.getProjectReferences(), }); const programs = [rootProgram]; // This might be a solution style tsconfig, in which case we need to add a program for each // reference so that the converter can look through each of these. if (rootProgram.getRootFileNames().length === 0) { logger.verbose("tsconfig appears to be a solution style tsconfig - creating programs for references"); const resolvedReferences = rootProgram.getResolvedProjectReferences(); for (const ref of resolvedReferences ?? []) { if (!ref) continue; // This indicates bad configuration... will be reported later. programs.push(ts.createProgram({ options: options.fixCompilerOptions(ref.commandLine.options), rootNames: ref.commandLine.fileNames, projectReferences: ref.commandLine.projectReferences, })); } } return programs; } /** * Expand a list of input files. * * Searches for directories in the input files list and replaces them with a * listing of all TypeScript files within them. One may use the ```--exclude``` option * to filter out files with a pattern. * * @param inputFiles The list of files that should be expanded. * @returns The list of input files with expanded directories. */ function expandInputFiles(logger, entryPoints, options, supportedFile) { const files = []; const exclude = createMinimatch(options.getValue("exclude")); function add(file, entryPoint) { let stats; try { stats = FS.statSync(file); } catch { // No permission or a symbolic link, do not resolve. return; } const fileIsDir = stats.isDirectory(); if (fileIsDir && !file.endsWith("/")) { file = `${file}/`; } if (fileIsDir) { FS.readdirSync(file).forEach((next) => { add(join(file, next), false); }); } else if (supportedFile.test(file)) { if (!entryPoint && matchesAny(exclude, file)) { return; } files.push(normalizePath(file)); } } entryPoints.forEach((file) => { const resolved = resolve(file); if (!FS.existsSync(resolved)) { logger.warn(logger.i18n.entry_point_0_did_not_exist(file)); return; } add(resolved, true); }); return files; }