'use strict'; const node_path = require('node:path'); const node_process = require('node:process'); const colorette = require('colorette'); const execa = require('execa'); const globby = require('globby'); const vite = require('vite'); const esToolkit = require('es-toolkit'); const uncrypto = require('uncrypto'); const GrayMatter = require('gray-matter'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } const GrayMatter__default = /*#__PURE__*/_interopDefaultCompat(GrayMatter); function pathEquals(path, equals) { return vite.normalizePath(path) === vite.normalizePath(equals); } function pathStartsWith(path, startsWith) { return vite.normalizePath(path).startsWith(vite.normalizePath(startsWith)); } function pathEndsWith(path, startsWith) { return vite.normalizePath(path).endsWith(vite.normalizePath(startsWith)); } function createHelpers(root, id) { const relativeId = node_path.relative(root, id); return { pathStartsWith, pathEquals, pathEndsWith, idEndsWith(endsWith) { return pathEndsWith(relativeId, endsWith); }, idEquals(equals) { return pathEquals(relativeId, equals); }, idStartsWith(startsWith) { return pathStartsWith(relativeId, startsWith); } }; } async function digestStringAsSHA256(message) { const msgUint8 = new TextEncoder().encode(message); const hashBuffer = await uncrypto.subtle.digest("SHA-256", msgUint8); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); return hashHex; } const defaultCommitURLHandler = (commit) => `${commit.repo_url}/commit/${commit.hash}`; const defaultReleaseTagURLHandler = (commit) => `${commit.repo_url}/releases/tag/${commit.tag}`; const defaultReleaseTagsURLHandler = (commit) => commit.tags?.map((tag) => `${commit.repo_url}/releases/tag/${tag}`); async function returnOrResolvePromise(val) { if (!(val instanceof Promise)) return val; return await val; } async function rewritePathsByPatterns(commit, path, patterns) { if (typeof patterns === "undefined" || patterns === null) return path; if ("handler" in patterns && typeof patterns.handler === "function") { const resolvedPath = await returnOrResolvePromise(patterns.handler(commit, path)); if (!resolvedPath) return path; return resolvedPath; } return path; } function rewritePathsByRewritingExtension(from, to) { return (_, path) => { const ext = node_path.extname(path); if (ext !== from) return path; return path.replace(new RegExp(`${from}$`), to); }; } function parseGitLogRefsAsTags(refs) { if (!refs) return []; const refsArray = refs.split(", ").map((ref) => ref.trim()); const tags = refsArray.filter((ref) => ref.startsWith("tag: ")); if (!tags) return []; return tags.map((tag) => tag.replace("tag: ", "").trim()); } function getRelativePath(file, srcDir, cwd) { cwd = vite.normalizePath(cwd); return file.replace(srcDir, "").replace(cwd, "").replace(/^\//, ""); } async function getRawCommitLogs(file, maxGitLogCount) { const fileDir = node_path.dirname(file); const fileName = node_path.basename(file); const format = "%H|%an|%ae|%ad|%s|%d|%b"; const { stdout } = await execa.execa("git", ["log", `--max-count=${maxGitLogCount ?? -1}`, `--format=${format}[GIT_LOG_COMMIT_END]`, "--follow", "--", fileName], { cwd: fileDir }); return stdout.replace(/\[GIT_LOG_COMMIT_END\]$/, "").split("[GIT_LOG_COMMIT_END]\n"); } function parseRawCommitLogs(path, rawLogs) { return rawLogs.filter((log) => !!log).map((raw) => { const [hash, author_name, author_email, date, message, refs, body] = raw.split("|").map((v) => v.trim()); return { path, hash, date, message, body, refs, author_name, author_email }; }); } function mergeRawCommits(rawCommits) { const commitMap = /* @__PURE__ */ new Map(); rawCommits.forEach((commit) => { const _commit = commitMap.get(commit.hash); if (_commit) _commit.paths.push(commit.path); else commitMap.set(commit.hash, { paths: [commit.path], ...esToolkit.omit(commit, ["path"]) }); }); return Array.from(commitMap.values()); } async function parseCommits(rawCommits, getRepoURL, getCommitURL, getReleaseTagURL, getReleaseTagsURL, mapContributors, optsRewritePathsBy) { const allAuthors = /* @__PURE__ */ new Map(); const resolvedCommits = await Promise.all(rawCommits.map(async (rawCommit) => { const { paths, hash, date, refs, message } = rawCommit; const resolvedCommit = { paths, hash, date_timestamp: new Date(date).getTime(), message, authors: [] }; if (typeof optsRewritePathsBy !== "undefined") await Promise.all(rawCommit.paths.map(async (p) => await rewritePathsByPatterns(resolvedCommit, p, optsRewritePathsBy))); resolvedCommit.repo_url = await returnOrResolvePromise(getRepoURL(resolvedCommit)) ?? "https://github.com/example/example"; resolvedCommit.hash_url = await returnOrResolvePromise(getCommitURL(resolvedCommit)) ?? defaultCommitURLHandler(resolvedCommit); const tags = parseGitLogRefsAsTags(refs?.replace(/[()]/g, "")); if (tags && tags.length > 0) { resolvedCommit.tags = tags; resolvedCommit.tag = resolvedCommit.tags?.[0] || void 0; resolvedCommit.release_tag_url = await returnOrResolvePromise(getReleaseTagURL(resolvedCommit)) ?? defaultReleaseTagURLHandler(resolvedCommit); resolvedCommit.release_tags_url = await returnOrResolvePromise(getReleaseTagsURL(resolvedCommit)) ?? defaultReleaseTagsURLHandler(resolvedCommit); } const authors = await parseCommitAuthors(rawCommit, mapContributors); authors.forEach((a) => allAuthors.set(a.name, esToolkit.omit(a, ["email"]))); resolvedCommit.authors = authors.map((a) => a.name); return resolvedCommit; })); return { commits: resolvedCommits, authors: [...allAuthors.values()] }; } async function parseCommitAuthors(commit, mapContributors) { const { author_name, author_email, body } = commit; const commitAuthor = { name: author_name, email: author_email }; const coAuthors = getCoAuthors(body); return await Promise.all([commitAuthor, ...coAuthors].filter((v) => !(v.name.match(/\[bot\]/i) || v.email?.match(/\[bot\]/i))).map(async (author) => { const targetCreatorByName = findMapAuthorByName(mapContributors, author.name); if (targetCreatorByName) { author.name = targetCreatorByName.name ?? author.name; author.i18n = findMapAuthorI18n(targetCreatorByName); author.url = findMapAuthorLink(targetCreatorByName); author.avatarUrl = await newAvatarForAuthor(targetCreatorByName, author.email); return author; } const targetCreatorByEmail = author.email && findMapAuthorByEmail(mapContributors, author.email); if (targetCreatorByEmail) { author.name = targetCreatorByEmail.name ?? author.name; author.i18n = findMapAuthorI18n(targetCreatorByEmail); author.url = findMapAuthorLink(targetCreatorByEmail); author.avatarUrl = await newAvatarForAuthor(targetCreatorByEmail, author.email); return author; } const targetCreatorByGitHub = findMapAuthorByGitHub(mapContributors, author.name, author.email); if (targetCreatorByGitHub) { author.name = targetCreatorByGitHub.name ?? author.name; author.i18n = findMapAuthorI18n(targetCreatorByGitHub); author.url = findMapAuthorLink(targetCreatorByGitHub); author.avatarUrl = await newAvatarForAuthor(targetCreatorByGitHub, author.email); return author; } author.avatarUrl = await newAvatarForAuthor(void 0, author.email); return author; })); } const multipleAuthorsRegex = /^ *Co-authored-by: ?([^<]*)<([^>]*)> */gim; function getCoAuthors(body) { if (!body) return []; return [...body.matchAll(multipleAuthorsRegex)].map((result) => { const [, name, email] = result; return { name: name.trim(), email: email.trim() }; }).filter((v) => !!v); } function findMapAuthorByName(mapContributors, author_name) { return mapContributors?.find((item) => { const res = item.mapByNameAliases && Array.isArray(item.mapByNameAliases) && item.mapByNameAliases.includes(author_name) || item.name === author_name || item.username === author_name; if (res) return true; return item.nameAliases && Array.isArray(item.nameAliases) && item.nameAliases.includes(author_name); }); } function findMapAuthorByEmail(mapContributors, author_email) { return mapContributors?.find((item) => { const res = item.mapByEmailAliases && Array.isArray(item.mapByEmailAliases) && item.mapByEmailAliases.includes(author_email); if (res) return true; return item.emailAliases && Array.isArray(item.emailAliases) && item.emailAliases.includes(author_email); }); } function findMapAuthorByGitHub(mapContributors, author_name, author_email) { const github = getGitHubUserNameFromNoreplyAddress(author_email); if (github && github.userName) { const mappedByName = findMapAuthorByName(mapContributors, github.userName); if (mappedByName && mappedByName.username) { mappedByName.username || (mappedByName.username = github.userName); return mappedByName; } return { name: author_name, username: github.userName }; } } function findMapAuthorLink(creator) { if (!creator.links && !!creator.username) return `https://github.com/${creator.username}`; if (typeof creator.links === "string" && !!creator.links) return creator.links; if (!Array.isArray(creator.links)) return; const priority = ["github", "twitter"]; for (const p of priority) { const matchedEntry = creator.links?.find((l) => l.type === p); if (matchedEntry) return matchedEntry.link; } return creator.links?.[0]?.link; } function findMapAuthorI18n(mappedAuthor) { if (mappedAuthor && mappedAuthor.i18n) { return mappedAuthor.i18n; } return void 0; } function getGitHubUserNameFromNoreplyAddress(email) { const match = email.match(/^(?:(?\d+)\+)?(?[a-zA-Z\d-]{1,39})@users.noreply.github.com$/); if (!match || !match.groups) return void 0; const { userName, userId } = match.groups; return { userId, userName }; } async function newAvatarForAuthor(mappedAuthor, email) { if (mappedAuthor) { if (mappedAuthor.avatar) return mappedAuthor.avatar; if (mappedAuthor.username) return `https://github.com/${mappedAuthor.username}.png`; } return `https://gravatar.com/avatar/${await digestStringAsSHA256(email)}?d=retro`; } const VirtualModuleID = "virtual:nolebase-git-changelog"; const ResolvedVirtualModuleId = `\0${VirtualModuleID}`; const logModulePrefix = `${colorette.cyan(`@nolebase/vitepress-plugin-git-changelog`)}${colorette.gray(":")}`; const logger = console; function GitChangelog(options = {}) { if (!options) options = {}; const { cwd = node_process.cwd(), maxGitLogCount, include = ["**/*.md", "!node_modules"], mapAuthors, repoURL = "https://github.com/example/example", getReleaseTagURL = defaultReleaseTagURLHandler, getReleaseTagsURL = defaultReleaseTagsURLHandler, getCommitURL = defaultCommitURLHandler, rewritePathsBy } = options; const getRepoURL = typeof repoURL === "function" ? repoURL : () => repoURL; const changelog = { commits: [], authors: [] }; const hotModuleReloadCachedCommits = /* @__PURE__ */ new Map(); let srcDir = ""; let config; const commitFromPath = async (paths) => { const rawCommits = (await Promise.all(paths.map(async (path) => { const rawLogs = await getRawCommitLogs(path, maxGitLogCount); const relativePath = getRelativePath(path, srcDir, cwd); const rawCommits2 = parseRawCommitLogs(relativePath, rawLogs); return rawCommits2; }))).flat(); const mergedRawCommits = mergeRawCommits(rawCommits); const resolvedCommits = parseCommits( mergedRawCommits, getRepoURL, getCommitURL, getReleaseTagURL, getReleaseTagsURL, mapAuthors, rewritePathsBy ); return resolvedCommits; }; return { name: "@nolebase/vitepress-plugin-git-changelog", config: () => ({ optimizeDeps: { exclude: [ "@nolebase/vitepress-plugin-git-changelog/client" ] }, ssr: { noExternal: [ "@nolebase/vitepress-plugin-git-changelog", // @nolebase/ui required here as noExternal to avoid the following error: // TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".vue" for ... // Read more here: https://github.com/vuejs/vitepress/issues/2915 // And here: https://stackblitz.com/edit/vite-gjz9zf?file=docs%2F.vitepress%2Fconfig.ts "@nolebase/ui" ] } }), configResolved(_config) { config = _config; srcDir = config.vitepress.srcDir; }, async buildStart() { if (config.command !== "build") return; const startsAt = Date.now(); logger.info(`${logModulePrefix} Prepare to gather git logs...`); if (changelog.commits.length > 0) return; await execa.execa("git", ["config", "--local", "core.quotepath", "false"]); const paths = await globby.globby(include, { gitignore: true, cwd, absolute: true }); Object.assign(changelog, await commitFromPath(paths)); const elapsed = Date.now() - startsAt; logger.info(`${logModulePrefix} Done. ${colorette.gray(`(${elapsed}ms)`)}`); }, resolveId(source) { if (source.startsWith(VirtualModuleID)) return `\0${source}`; }, load(id) { if (!id.startsWith(ResolvedVirtualModuleId)) return null; return `export default ${JSON.stringify(changelog, null, config.isProduction ? 0 : 2)}`; }, configureServer(server) { server.ws.on("nolebase-git-changelog:client-mounted", async (data) => { if (!data || typeof data !== "object") return; if (!("page" in data && "filePath" in data.page)) return; let result = hotModuleReloadCachedCommits.get(data.page.filePath); if (!result) { const path = vite.normalizePath(node_path.join(srcDir, data.page.filePath)); result = await commitFromPath([path]); hotModuleReloadCachedCommits.set(data.page.filePath, result); } Object.assign(changelog, result); if (!result.commits.length) return; const virtualModule = server.moduleGraph.getModuleById(ResolvedVirtualModuleId); if (!virtualModule) return; server.moduleGraph.invalidateModule(virtualModule); server.ws.send({ type: "custom", event: "nolebase-git-changelog:updated", data: changelog }); }); } }; } function GitChangelogMarkdownSection(options) { const { excludes = ["index.md"], exclude = () => false } = options ?? {}; let root = ""; return { name: "@nolebase/vitepress-plugin-git-changelog-markdown-section", // May set to 'pre' since end user may use vitepress wrapped vite plugin to // specify the plugins, which may cause this plugin to be executed after // vitepress or the other markdown processing plugins. enforce: "pre", configResolved(config) { root = config.root ?? ""; }, transform(code, id) { if (!id.endsWith(".md")) return null; const helpers = createHelpers(root, id); if (excludes.includes(node_path.relative(root, id))) return null; if (exclude(id, { helpers })) return null; const parsedMarkdownContent = GrayMatter__default(code); if ("nolebase" in parsedMarkdownContent.data && "gitChangelog" in parsedMarkdownContent.data.nolebase && !parsedMarkdownContent.data.nolebase.gitChangelog) return null; if ("gitChangelog" in parsedMarkdownContent.data && !parsedMarkdownContent.data.gitChangelog) return null; if (!options?.sections?.disableContributors) code = TemplateContributors(code); if (!options?.sections?.disableChangelog) code = TemplateChangelog(code); return code; } }; } function TemplateContributors(code) { return `${code} `; } function TemplateChangelog(code) { return `${code} `; } exports.GitChangelog = GitChangelog; exports.GitChangelogMarkdownSection = GitChangelogMarkdownSection; exports.rewritePathsByRewritingExtension = rewritePathsByRewritingExtension; //# sourceMappingURL=index.cjs.map