UNPKG

typedoc

Version:

Create api documentation for TypeScript projects.

293 lines (292 loc) 12 kB
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); var _, done = false; for (var i = decorators.length - 1; i >= 0; i--) { var context = {}; for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; for (var p in contextIn.access) context.access[p] = contextIn.access[p]; context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); if (kind === "accessor") { if (result === void 0) continue; if (result === null || typeof result !== "object") throw new TypeError("Object expected"); if (_ = accept(result.get)) descriptor.get = _; if (_ = accept(result.set)) descriptor.set = _; if (_ = accept(result.init)) initializers.unshift(_); } else if (_ = accept(result)) { if (kind === "field") initializers.unshift(_); else descriptor[key] = _; } } if (target) Object.defineProperty(target, contextIn.name, descriptor); done = true; }; var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { var useValue = arguments.length > 2; for (var i = 0; i < initializers.length; i++) { value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); } return useValue ? value : void 0; }; import { spawnSync } from "child_process"; import { normalizePath } from "../../utils/index.js"; import { NonEnumerable } from "../../utils/general.js"; import { dirname, join } from "path"; import { existsSync } from "fs"; const TEN_MEGABYTES = 1024 * 10000; function git(...args) { return spawnSync("git", args, { encoding: "utf-8", windowsHide: true, maxBuffer: TEN_MEGABYTES, }); } let haveGit; export function gitIsInstalled() { haveGit ??= git("--version").status === 0; return haveGit; } export class AssumedRepository { path; gitRevision; sourceLinkTemplate; constructor(path, gitRevision, sourceLinkTemplate) { this.path = path; this.gitRevision = gitRevision; this.sourceLinkTemplate = sourceLinkTemplate; } getURL(fileName, line) { const replacements = { gitRevision: this.gitRevision, "gitRevision:short": this.gitRevision.substring(0, 8), path: fileName.substring(this.path.length + 1), line, }; return this.sourceLinkTemplate.replace(/\{(gitRevision|gitRevision:short|path|line)\}/g, (_, key) => replacements[key]); } } /** * Stores data of a repository. */ let GitRepository = (() => { let _files_decorators; let _files_initializers = []; let _files_extraInitializers = []; return class GitRepository { static { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; _files_decorators = [NonEnumerable]; __esDecorate(null, null, _files_decorators, { kind: "field", name: "files", static: false, private: false, access: { has: obj => "files" in obj, get: obj => obj.files, set: (obj, value) => { obj.files = value; } }, metadata: _metadata }, _files_initializers, _files_extraInitializers); if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); } /** * The path of this repository on disk. */ path; /** * All files tracked by the repository. */ files = __runInitializers(this, _files_initializers, new Set()); urlTemplate = __runInitializers(this, _files_extraInitializers); gitRevision; /** * Create a new Repository instance. * * @param path The root path of the repository. */ constructor(path, gitRevision, urlTemplate) { this.path = path; this.gitRevision = gitRevision; this.urlTemplate = urlTemplate; const out = git("-C", path, "ls-files", "-z"); if (out.status === 0) { out.stdout.split("\0").forEach((file) => { if (file !== "") { this.files.add(normalizePath(path + "/" + file)); } }); } } /** * Get the URL of the given file on GitHub or Bitbucket. * * @param fileName The file whose URL should be determined. * @returns A URL pointing to the web preview of the given file or undefined. */ getURL(fileName, line) { if (!this.files.has(fileName)) { return; } const replacements = { gitRevision: this.gitRevision, "gitRevision:short": this.gitRevision.substring(0, 8), path: fileName.substring(this.path.length + 1), line, }; return this.urlTemplate.replace(/\{(gitRevision|gitRevision:short|path|line)\}/g, (_, key) => replacements[key]); } /** * Try to create a new repository instance. * * Checks whether the given path is the root of a valid repository and if so * creates a new instance of {@link GitRepository}. * * @param path The potential repository root. * @returns A new instance of {@link GitRepository} or undefined. */ static tryCreateRepository(path, sourceLinkTemplate, gitRevision, gitRemote, logger) { gitRevision ||= git("-C", path, "rev-parse", "HEAD").stdout.trim(); if (!gitRevision) return; // Will only happen in a repo with no commits. let urlTemplate; if (sourceLinkTemplate) { urlTemplate = sourceLinkTemplate; } else { const remotesOut = git("-C", path, "remote", "get-url", gitRemote); if (remotesOut.status === 0) { urlTemplate = guessSourceUrlTemplate(remotesOut.stdout.split("\n")); } else { logger.warn(logger.i18n.git_remote_0_not_valid(gitRemote)); } } if (!urlTemplate) return; return new GitRepository(normalizePath(path), gitRevision, urlTemplate); } }; })(); export { GitRepository }; /** * Responsible for keeping track of 0-N repositories which exist on a machine. * This used to be inlined in SourcePlugin, moved out for easy unit testing. * * Git repositories can be nested. Files should be resolved to a repo as shown * below: * ```text * /project * /project/.git (A) * /project/file.js (A) * /project/folder/file.js (A) * /project/sub/.git (B) * /project/sub/file.js (B) * ``` * * In order words, it is not safe to assume that just because a file is within * the `/project` directory, that it belongs to repo `A`. As calling git is * expensive (~20-300ms depending on the machine, antivirus, etc.) we check for * `.git` folders manually, and only call git if one is found. * * Symlinked files have the potential to further complicate this. If TypeScript's * `preserveSymlinks` option is on, then this may be passed the path to a symlinked * file. Unlike TypeScript, we will resolve the path, as the repo link should really * point to the actual file. */ export class RepositoryManager { basePath; gitRevision; gitRemote; sourceLinkTemplate; disableGit; logger; cache = new Map(); assumedRepo; constructor(basePath, gitRevision, gitRemote, sourceLinkTemplate, disableGit, logger) { this.basePath = basePath; this.gitRevision = gitRevision; this.gitRemote = gitRemote; this.sourceLinkTemplate = sourceLinkTemplate; this.disableGit = disableGit; this.logger = logger; this.assumedRepo = new AssumedRepository(this.basePath, this.gitRevision, this.sourceLinkTemplate); } /** * Check whether the given file is placed inside a repository. * * @param fileName The name of the file a repository should be looked for. * @returns The found repository info or undefined. */ getRepository(fileName) { if (this.disableGit) { return this.assumedRepo; } return this.getRepositoryFolder(normalizePath(dirname(fileName))); } getRepositoryFolder(dir) { if (this.cache.has(dir)) { return this.cache.get(dir); } if (existsSync(join(dir, ".git"))) { // This might just be a git repo, or we might be in some self-recursive symlink // loop, and the repo is actually somewhere else. Ask Git where the repo actually is. const repo = git("-C", dir, "rev-parse", "--show-toplevel"); if (repo.status === 0) { const repoDir = repo.stdout.replace("\n", ""); // This check is only necessary if we're in a symlink loop, otherwise // it will always be true. if (!this.cache.has(repoDir)) { this.cache.set(repoDir, GitRepository.tryCreateRepository(repoDir, this.sourceLinkTemplate, this.gitRevision, this.gitRemote, this.logger)); } this.cache.set(dir, this.cache.get(repoDir)); } else { // Not a git repo, probably corrupt. this.cache.set(dir, undefined); } } else { // We may be at the root of the file system, in which case there is no repo. this.cache.set(dir, undefined); this.cache.set(dir, this.getRepositoryFolder(dirname(dir))); } return this.cache.get(dir); } } // Should have three capturing groups: // 1. hostname // 2. user // 3. project const repoExpressions = [ /(github(?!.us)(?:\.[a-z]+)*\.[a-z]{2,})[:/]([^/]+)\/(.*)/, /(\w+\.githubprivate.com)[:/]([^/]+)\/(.*)/, // GitHub enterprise /(\w+\.ghe.com)[:/]([^/]+)\/(.*)/, // GitHub enterprise /(\w+\.github.us)[:/]([^/]+)\/(.*)/, // GitHub enterprise /(bitbucket.org)[:/]([^/]+)\/(.*)/, /(gitlab.com)[:/]([^/]+)\/(.*)/, ]; export function guessSourceUrlTemplate(remotes) { let hostname = ""; let user = ""; let project = ""; outer: for (const repoLink of remotes) { for (const regex of repoExpressions) { const match = regex.exec(repoLink); if (match) { hostname = match[1]; user = match[2]; project = match[3]; break outer; } } } if (!hostname) return; if (project.endsWith(".git")) { project = project.slice(0, -4); } let sourcePath = "blob"; let anchorPrefix = "L"; if (hostname.includes("gitlab")) { sourcePath = "-/blob"; } else if (hostname.includes("bitbucket")) { sourcePath = "src"; anchorPrefix = "lines-"; } return `https://${hostname}/${user}/${project}/${sourcePath}/{gitRevision}/{path}#${anchorPrefix}{line}`; }