diff --git a/apps/www/package.json b/apps/www/package.json index f5ff8770c..a1f784904 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "prebuild": "cd ../.. && npm run build:apsara", + "prebuild": "cd ../.. && (git rev-parse -q --verify refs/tags/v1.0.0 >/dev/null || git fetch -q --tags $(test \"$(git rev-parse --is-shallow-repository)\" = true && echo --unshallow) https://github.com/raystack/apsara.git || true) && npm run build:apsara", "dev": "next dev", "build": "next build", "start": "next start", diff --git a/apps/www/src/components/docs/changelog.tsx b/apps/www/src/components/docs/changelog.tsx new file mode 100644 index 000000000..c737cf52d --- /dev/null +++ b/apps/www/src/components/docs/changelog.tsx @@ -0,0 +1,107 @@ +import { execFileSync } from 'node:child_process'; +import Link from 'fumadocs-core/link'; +import styles from '../mdx/mdx-components.module.css'; + +const RELEASES_URL = 'https://github.com/raystack/apsara/releases'; +const PULL_URL = 'https://github.com/raystack/apsara/pull'; + +// Releases before v1.0.0 are the Radix-based library. +const FIRST_TAG = 'v1.0.0'; +const TYPES = new Set(['feat', 'fix', 'refactor']); +const SUBJECT = + /^(\w+)(?:\(([^)]*)\))?!?:\s*(?:\[([^\]]*)\]\s*)?(.+?)\s*\(#(\d+)\)$/; + +interface ChangelogEntry { + type: string; + scopes: string[]; + summary: string; + pr: number; +} + +interface ChangelogRelease { + /** `null` for the changes after the latest release. */ + tag: string | null; + entries: ChangelogEntry[]; +} + +/** + * Reads a squash-merged subject, `: [] (#)` or + * `(): (#)`. + */ +function parseSubject(subject: string): ChangelogEntry | undefined { + const match = SUBJECT.exec(subject); + if (!match || !TYPES.has(match[1])) return undefined; + const [, type, parenScopes, bracketScopes, summary, pr] = match; + const scopes = (bracketScopes ?? parenScopes ?? '') + .split(',') + .map(scope => scope.trim()) + .filter(Boolean); + return { type, scopes, summary, pr: Number(pr) }; +} + +function git(args: string[]) { + return execFileSync('git', args, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'] + }); +} + +/** + * Returns the feat, fix and refactor pull requests of each release after + * v1.0.0, newest first, or `null` when the build has no git history or tags. + */ +function getChangelog(): ChangelogRelease[] | null { + try { + const tags = git(['tag', '--merged', 'HEAD', '--sort=-v:refname']) + .split('\n') + .filter(tag => /^v\d+\.\d+\.\d+$/.test(tag)); + const first = tags.indexOf(FIRST_TAG); + if (first === -1) return null; + const ranges: [string | null, string][] = [[null, `${tags[0]}..HEAD`]]; + for (let index = 0; index < first; index += 1) + ranges.push([tags[index], `${tags[index + 1]}..${tags[index]}`]); + return ranges + .map(([tag, range]) => ({ + tag, + entries: git(['log', '--no-merges', '--format=%s', range]) + .split('\n') + .flatMap(subject => parseSubject(subject) ?? []) + })) + .filter(release => release.entries.length > 0); + } catch { + return null; + } +} + +/** The pull requests of each release, from the git history. */ +export function Changelog() { + const releases = getChangelog(); + if (!releases) + return ( +

+ See GitHub Releases. +

+ ); + + return releases.map(release => ( +
+

+ {release.tag ? ( + {release.tag} + ) : ( + 'Unreleased' + )} +

+
    + {release.entries.map(entry => ( +
  • + {entry.type}:{' '} + {entry.scopes.length > 0 && `[${entry.scopes.join(', ')}] `} + {entry.summary} ( + #{entry.pr}) +
  • + ))} +
+
+ )); +} diff --git a/apps/www/src/components/mdx/mdx-components.tsx b/apps/www/src/components/mdx/mdx-components.tsx index da56373ef..0b053efb6 100644 --- a/apps/www/src/components/mdx/mdx-components.tsx +++ b/apps/www/src/components/mdx/mdx-components.tsx @@ -11,6 +11,7 @@ import type { TableHTMLAttributes } from 'react'; import Demo from '../demo'; +import { Changelog } from '../docs/changelog'; import { IconGallery } from '../icongallery'; import { TokenTable } from '../tokentable'; import { TypeTable } from '../typetable'; @@ -104,6 +105,7 @@ const mdxComponents = { ), IconGallery, + Changelog, Callout: (props: ComponentPropsWithoutRef) => ( diff --git a/apps/www/src/content/docs/(overview)/meta.json b/apps/www/src/content/docs/(overview)/meta.json index d149ba563..21dbd2e4e 100644 --- a/apps/www/src/content/docs/(overview)/meta.json +++ b/apps/www/src/content/docs/(overview)/meta.json @@ -1,4 +1,4 @@ { "title": "Overview", - "pages": ["..."] + "pages": ["...", "changelog"] }