From e4b5eac4ef98fdd3679d9668f4dc4d0d1a2e03b8 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 08:22:38 +0200 Subject: [PATCH 01/28] docs: design spec for GitlabProjectInfo embedded sections Add design for opt-in releases/commits/issues sections embedded in the GitlabProjectInfo card, plus a link override. Sections are count-gated (no fetch when unset/<=0), reuse fetchReleases/fetchIssues, add new CommitData + client.getCommits, and support list/cards layouts. Signed-off-by: Thomas Decaux --- .../2026-07-08-projectinfo-sections-design.md | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-08-projectinfo-sections-design.md diff --git a/docs/superpowers/specs/2026-07-08-projectinfo-sections-design.md b/docs/superpowers/specs/2026-07-08-projectinfo-sections-design.md new file mode 100644 index 0000000..06f6fab --- /dev/null +++ b/docs/superpowers/specs/2026-07-08-projectinfo-sections-design.md @@ -0,0 +1,185 @@ +# GitlabProjectInfo: embedded releases / commits / issues sections + +**Date:** 2026-07-08 +**Status:** Approved design +**Component:** `` + +## Goal + +Let authors optionally embed compact, build-time-fetched summaries of a +project's latest releases, commits, and issues directly inside the +`GitlabProjectInfo` card, and let them override the card's title link. Each +section is opt-in via a count attribute, discreet by default, and configurable +to a richer layout. + +New MDX attributes on ``: + +| Attribute | Type | Default | Effect | +|---|---|---|---| +| `releases` | number | unset | Include latest N releases. Unset/`≤0` ⇒ not fetched, not rendered. | +| `commits` | number | unset | Include latest N commits. Unset/`≤0` ⇒ not fetched, not rendered. | +| `issues` | number | unset | Include latest N issues. Unset/`≤0` ⇒ not fetched, not rendered. | +| `link` | string | `data.webUrl` | Override the card title's `href`. | +| `releasesLayout` | `"list"` \| `"cards"` | `"list"` | Compact one-line list vs richer cards. | +| `commitsLayout` | `"list"` \| `"cards"` | `"list"` | Compact one-line list vs richer rows. | +| `issuesLayout` | `"list"` \| `"cards"` | `"list"` | Compact one-line list vs richer cards. | + +Attribute values are static scalar literals only (string/number/boolean), per +the existing `src/remark/attributes.ts` parser — no objects or arrays. + +Example: + +```mdx + + +``` + +## Chosen approach + +**Compose existing fetchers.** `fetchProjectInfo` remains the single fetcher +registered for `GitlabProjectInfo`. When a section's count attribute is present +and `> 0`, it awaits the matching fetcher and attaches the result to +`ProjectInfoData`: + +- releases → reuse existing `fetchReleases(ctx, { project, limit })` +- issues → reuse existing `fetchIssues(ctx, { project, limit })` +- commits → new `fetchCommits(ctx, { project, limit })` + new client method + +Rejected alternatives: + +- **Inline everything** in `fetchProjectInfo` — duplicates the + snake_case→camelCase normalization already living in `fetchReleases` / + `fetchIssues`. +- **Separate injected props** — the remark plugin injects exactly one `data` + prop per JSX element, so the sub-data must be nested inside `ProjectInfoData`. + +## Data model & fetching + +### Types (`src/gitlab/types.ts`) + +New `CommitData`: + +```ts +export interface CommitData { + shortId: string; // e.g. "a79a7f7" + title: string; + webUrl: string; // link to the commit + authorName: string; + createdAt: string; // ISO; rendered as a short/relative date +} +``` + +`ProjectInfoData` gains three optional fields, populated only when the matching +count attribute is set and `> 0`: + +```ts +export interface ProjectInfoData { + // …existing fields unchanged… + releases?: ReleaseData[]; + commits?: CommitData[]; + issues?: IssueData[]; +} +``` + +`ReleaseData` and `IssueData` are reused as-is. + +### Client (`src/gitlab/client.ts`) + +New method mirroring `getReleases` / `getIssues`: + +```ts +async getCommits(project: ProjectRef, limit: number): Promise { + const commits = await this.api.Commits.all(project, { perPage: limit, maxPages: 1 }); + return commits.slice(0, limit); +} +``` + +### Fetcher (`src/gitlab/fetchers.ts`) + +- Add `fetchCommits(ctx, attrs)` normalizing gitbeaker commits + (`short_id`, `title`, `web_url`, `author_name`, `created_at`) → + `CommitData`, memoized on `commits:${project}:${limit}`. +- Extend `fetchProjectInfo` to read `releases` / `commits` / `issues` as + numbers. For each present and `> 0`, await the corresponding fetcher (passing + `limit: N`) and attach the array to the result. +- **No-fetch rule (explicit):** if a count is unset or `≤ 0`, that section's + fetcher is **not called** — no `getCommits` / releases / issues request is + made, and the field is left `undefined`. +- Extend the `projectInfo:` cache key to include the three counts + (e.g. `projectInfo:${project}:r${rN}:c${cN}:i${iN}`) so different configs do + not collide. `link` and the `*Layout` attributes are presentational and are + **not** part of the cache key. + +## Component rendering (`src/components/GitlabProjectInfo.tsx`) + +The three sections render **inside the card, immediately after the +`descriptionHtml` block**, in fixed order: **releases → commits → issues**. A +section renders only if its data array is present and non-empty. + +New presentational props read directly from MDX attributes (not from `data`): + +- `link?: string` — overrides the title `href`; falls back to `data.webUrl`. +- `releasesLayout?`, `commitsLayout?`, `issuesLayout?` — `"list"` (default) or + `"cards"`. Invalid literals throw at build time, matching the existing + `GitlabLabels` layout validation. + +Each section has a small heading label ("Releases", "Latest commits", +"Issues"). + +### Compact (`list`) lines — the discreet default + +- **Release:** `tagName` — `name`, linked. +- **Commit:** `shortId` (linked to commit) · `title` · `authorName` · short date. +- **Issue:** `#iid` `title`, linked to `webUrl`. + +### Rich (`cards`) + +Self-contained renderers inside `GitlabProjectInfo` (per the requirement that +the sections live inside this component): + +- Releases and issues render as small stacked cards. +- Commits render as richer rows (SHA + title + author + date). + +Shared per-item markup lives in tiny local helpers to avoid duplication between +the two layouts. + +## Error handling + +- Composed sub-fetches run through the existing `strict` path: in `strict` + mode a failed releases/commits/issues fetch aborts the build (current + behavior); in non-strict/dev mode the failing section is omitted and the rest + of the card still renders. +- A failure of the core project-info fetch still yields the `error` prop → + `Fallback`, unchanged. +- Invalid `*Layout` literals throw at build time. + +## Testing (TDD) + +- **Client:** `getCommits` calls `Commits.all` with `perPage`/`maxPages` and + slices to `limit` (mocked gitbeaker). +- **Fetcher:** + - `fetchProjectInfo` attaches `releases` / `commits` / `issues` only when the + count is `> 0`. + - Asserts **no** client call for a section when its count is unset or `0`. + - Cache key varies by counts. + - `fetchCommits` normalizes snake_case → camelCase. +- **Component:** + - Sections render after the description in order releases → commits → issues. + - Compact vs `cards` layouts (queries by role/text). + - `link` overrides the title `href`; default is `data.webUrl`. + - Empty/absent arrays render nothing. + - Invalid layout literal throws. + +## Documentation + +- Update `README` with the new attributes and examples. +- Update the `GitlabProjectInfo` page under + `examples/site/docs/components/`; the e2e Docusaurus build + (`test/e2e/build.test.ts`) exercises the render path. + +## Out of scope + +- No standalone `GitlabCommits` component (commits live only inside + `GitlabProjectInfo`). +- No new pagination ceiling work; each section fetches a single page bounded by + its `limit` (`maxPages: 1`), consistent with `getReleases` / `getIssues`. From 9a998f0f7910668846cd72ed58d1f7cce9231aab Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 08:32:04 +0200 Subject: [PATCH 02/28] docs: add extended project stats to GitlabProjectInfo spec Fold a second feature into the design: append commits/contributors/open-issues/ repository-size pills to the stats row (gated by showStats, best-effort, never aborts the build). Adds getContributorsCount + statistics option on getProject; drops code-lines (no GitLab LOC API). Signed-off-by: Thomas Decaux --- .../2026-07-08-projectinfo-sections-design.md | 129 +++++++++++++++++- 1 file changed, 125 insertions(+), 4 deletions(-) diff --git a/docs/superpowers/specs/2026-07-08-projectinfo-sections-design.md b/docs/superpowers/specs/2026-07-08-projectinfo-sections-design.md index 06f6fab..06300c6 100644 --- a/docs/superpowers/specs/2026-07-08-projectinfo-sections-design.md +++ b/docs/superpowers/specs/2026-07-08-projectinfo-sections-design.md @@ -1,9 +1,20 @@ -# GitlabProjectInfo: embedded releases / commits / issues sections +# GitlabProjectInfo: embedded sections + extended stats **Date:** 2026-07-08 **Status:** Approved design **Component:** `` +Two related enhancements to ``: + +1. **Embedded sections** — opt-in latest releases / commits / issues rendered + inside the card, plus a title-link override. +2. **Extended stats** — additional pills (commits, contributors, open issues, + repository size) appended to the existing stats row when available. + +--- + +# Feature 1 — Embedded releases / commits / issues sections + ## Goal Let authors optionally embed compact, build-time-fetched summaries of a @@ -177,9 +188,119 @@ the two layouts. `examples/site/docs/components/`; the e2e Docusaurus build (`test/e2e/build.test.ts`) exercises the render path. -## Out of scope +--- + +# Feature 2 — Extended project stats + +## Goal + +Enrich the existing `gitlab-stats` row in `GitlabProjectInfo` with more +project metrics, sourced cheaply from the GitLab API. The existing `showStats` +prop still gates the whole row; the extra pills are appended automatically +whenever their data is available. No new opt-in attributes. + +Stats added: **commits count**, **contributors count**, **open issues count**, +**repository size**. (Code-lines count was requested but is **not feasible** — +GitLab exposes no lines-of-code API; `repository_size` is bytes-on-disk, +`languages` is percentages, and contributor `additions`/`deletions` are churn, +not current LOC.) + +## API feasibility (verified against installed gitbeaker) + +| Stat | Source | Cost | +|---|---|---| +| Commits count | `statistics.commit_count` on `Projects.show(project, {statistics:true})` | Free (same call) | +| Repository size | `statistics.repository_size` (bytes) on the same call | Free (same call) | +| Open issues count | `open_issues_count` on the base project object | Free (already fetched) | +| Contributors count | `X-Total` pagination header from `Repositories.allContributors` | 1 cheap request | + +**Permission caveat:** the `statistics` object is only returned when the +build-time token has **Reporter+** access. For anonymous/public builds it is +omitted, so `commitCount` / `repositorySize` are `undefined` and their pills +are simply not rendered. + +## Data model (`src/gitlab/types.ts`) + +`ProjectInfoData` gains four optional, best-effort fields (omitted when +unavailable): + +```ts +export interface ProjectInfoData { + // …existing: starCount, forksCount, lastActivityAt… + openIssuesCount?: number; // project.open_issues_count (only when issues_enabled) + commitCount?: number; // statistics.commit_count — needs Reporter+ token + repositorySize?: number; // statistics.repository_size (bytes) — needs Reporter+ token + contributorsCount?: number; // from the contributors endpoint's X-Total header +} +``` + +## Client (`src/gitlab/client.ts`) + +- `getProject(project, opts?)` gains an optional `{ statistics?: boolean }`. + `fetchProjectInfo` passes `{ statistics: true }`; all other callers + (readme / file / labels) keep today's behavior — no extra cost or permission + change elsewhere. +- New `getContributorsCount(project): Promise`: + calls `Repositories.allContributors(project, { showExpanded: true, perPage: 1, maxPages: 1 })` + and returns `paginationInfo.total` (the `X-Total` header). Returns `undefined` + when the header is absent — no full-list walk. + +## Fetcher (`fetchProjectInfo`) + +After the project fetch (now with `statistics: true`), map: + +- `commitCount` ← `p.statistics?.commit_count` (undefined when statistics withheld) +- `repositorySize` ← `p.statistics?.repository_size` (same) +- `openIssuesCount` ← `p.open_issues_count` only when `p.issues_enabled` +- `contributorsCount` ← `await getContributorsCount(project)` + +**All four are best-effort:** any absence or failure leaves the field +`undefined`, renders no pill, and **never aborts the build — even in `strict` +mode** (they are supplementary, unlike the Feature 1 section fetches, which +respect `strict`). The contributors call is wrapped so a failure degrades to +`undefined`. No cache-key change — these are deterministic per project and +already covered by the existing `projectInfo:` key. + +## Component (`src/components/GitlabProjectInfo.tsx`) + +Inside the existing `showStats` block, append one pill per **defined** field: + +- Commits — `formatCount(commitCount)` (e.g. "1.2k commits") +- Contributors — `formatCount(contributorsCount)` (e.g. "8 contributors") +- Open issues — `formatCount(openIssuesCount)` (e.g. "12 issues") +- Repository size — new `formatBytes(repositorySize)` helper (e.g. "4.2 MB") + +`showStats={false}` still hides the entire row. Existing star / fork / updated +pills are unchanged. + +## Testing (TDD) + +- **Client:** `getProject` forwards `statistics: true` when requested; + `getContributorsCount` returns the pagination `total` and `undefined` when the + header is absent. +- **Fetcher:** maps all four fields; omits `commitCount`/`repositorySize` when + `statistics` is absent; omits `openIssuesCount` when issues disabled; a + contributors-fetch failure yields `undefined` and does **not** throw (even in + strict mode). +- **Component:** each pill renders only when its value is defined; `formatBytes` + formatting (bytes → KB/MB/GB); `showStats={false}` hides the row. +- **`formatBytes`:** unit-tested directly (0, bytes, KB, MB, GB boundaries). + +## Documentation + +- Update the `GitlabProjectInfo` README section and the + `examples/site/docs/components/` page to describe the new stat pills and the + Reporter+ token requirement for commits/size. + +--- + +# Out of scope (both features) - No standalone `GitlabCommits` component (commits live only inside `GitlabProjectInfo`). -- No new pagination ceiling work; each section fetches a single page bounded by - its `limit` (`maxPages: 1`), consistent with `getReleases` / `getIssues`. +- No new pagination ceiling work for the sections; each fetches a single page + bounded by its `limit` (`maxPages: 1`), consistent with `getReleases` / + `getIssues`. +- No code-lines-count stat (no GitLab API for LOC). +- No new opt-in attributes for stats; visibility is governed by `showStats` + plus data availability. From 92f80ce691b0246781c916b824faf19a37fb837c Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 08:46:49 +0200 Subject: [PATCH 03/28] docs: implementation plan for GitlabProjectInfo sections + stats Signed-off-by: Thomas Decaux --- ...26-07-08-projectinfo-sections-and-stats.md | 1062 +++++++++++++++++ 1 file changed, 1062 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-08-projectinfo-sections-and-stats.md diff --git a/docs/superpowers/plans/2026-07-08-projectinfo-sections-and-stats.md b/docs/superpowers/plans/2026-07-08-projectinfo-sections-and-stats.md new file mode 100644 index 0000000..8b9ec14 --- /dev/null +++ b/docs/superpowers/plans/2026-07-08-projectinfo-sections-and-stats.md @@ -0,0 +1,1062 @@ +# GitlabProjectInfo Sections + Extended Stats Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add opt-in embedded releases/commits/issues sections (with a title-link override) and extra stat pills (commits, contributors, open issues, repo size) to ``. + +**Architecture:** `fetchProjectInfo` stays the single fetcher for the element. It composes the existing `fetchReleases`/`fetchIssues` plus a new `fetchCommits` for the sections, and reads project `statistics` + a contributors count for the stats. All extra data is attached to `ProjectInfoData` as optional fields. The pure `GitlabProjectInfo` component renders sections (compact `list` default, opt-in `cards`) right after the description, and appends stat pills inside the existing `showStats` row. Section attributes are count-gated (no fetch when unset/≤0); stats are best-effort (never abort the build). + +**Tech Stack:** TypeScript (ESM, `.js` import specifiers), `@gitbeaker/rest`, React (SSR), Vitest + React Testing Library. + +**Spec:** `docs/superpowers/specs/2026-07-08-projectinfo-sections-design.md` + +**Conventions (read before starting):** +- ESM-only: every intra-package import uses an explicit `.js` extension. +- gitbeaker responses are snake_case; normalize to camelCase in fetchers. +- Component attribute values are static literals only; the remark plugin injects a `data` prop and leaves all other MDX attributes (e.g. `link`, `releasesLayout`, `showStats`) as props on the element — they flow straight to the React component. +- Component styling uses plain global class names (e.g. `gitlab-badge`, `gitlab-muted`, `gitlab-title`) — this file does NOT use CSS modules. Reuse existing classes; add new semantic class names freely (no new CSS file required). +- After each task run `npx vitest run ` for the touched tests; run `npm run typecheck` before the final commit of each feature. +- Commits are GPG-signed automatically (`commit.gpgsign=true`). Verify with `git log -1 --format='%G?'` (expect `G`). + +--- + +# FEATURE 1 — Embedded releases / commits / issues sections + +## File structure (Feature 1) + +- Modify `src/gitlab/client.ts` — add `getCommits`. +- Modify `src/gitlab/types.ts` — add `CommitData`; add `releases`/`commits`/`issues` to `ProjectInfoData`. +- Modify `src/gitlab/fetchers.ts` — add `fetchCommits`; compose sections + validate layouts in `fetchProjectInfo`. +- Modify `src/components/GitlabProjectInfo.tsx` — render sections + `link` override. +- Modify `src/components/types.ts`, `src/components/index.ts`, `src/index.ts` — re-export `CommitData`. +- Tests: `src/gitlab/client.test.ts`, `src/gitlab/fetchers.test.ts`, `src/components/GitlabProjectInfo.test.tsx`. +- Docs: `README.md`, `examples/site/docs/components/*ProjectInfo*`. + +--- + +## Task 1: `getCommits` client method + +**Files:** +- Modify: `src/gitlab/client.ts` (add method after `getIssues`) +- Test: `src/gitlab/client.test.ts` + +- [ ] **Step 1: Write the failing test** + +In `src/gitlab/client.test.ts`, add a `commitsAllMock` alongside the other mocks. Add it to the mocked `Gitlab` return object as `Commits: { all: commitsAllMock }`, and reset it in `beforeEach` (`commitsAllMock.mockReset()`). Then add this test inside `describe("GitLabClient", ...)`: + +```ts +it("getCommits fetches one page and slices to the limit", async () => { + commitsAllMock.mockResolvedValue([ + { short_id: "a1", title: "one" }, + { short_id: "b2", title: "two" }, + { short_id: "c3", title: "three" }, + ]); + const client = new GitLabClient({ host: "https://gitlab.com" }); + const commits = await client.getCommits("g/r", 2); + expect(commitsAllMock).toHaveBeenCalledWith("g/r", { perPage: 2, maxPages: 1 }); + expect(commits).toHaveLength(2); + expect(commits[0].short_id).toBe("a1"); +}); +``` + +Declare the mock at the top with the others: `const commitsAllMock = vi.fn();`. + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npx vitest run src/gitlab/client.test.ts -t "getCommits"` +Expected: FAIL — `client.getCommits is not a function`. + +- [ ] **Step 3: Implement `getCommits`** + +In `src/gitlab/client.ts`, add after `getIssues`: + +```ts + async getCommits(project: ProjectRef, limit: number): Promise { + const commits = await this.api.Commits.all(project, { perPage: limit, maxPages: 1 }); + return commits.slice(0, limit); + } +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npx vitest run src/gitlab/client.test.ts -t "getCommits"` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/gitlab/client.ts src/gitlab/client.test.ts +git commit -m "feat: add getCommits to GitLabClient" +``` + +--- + +## Task 2: `CommitData` type + `fetchCommits` fetcher + +**Files:** +- Modify: `src/gitlab/types.ts` (add `CommitData`) +- Modify: `src/gitlab/fetchers.ts` (add `fetchCommits`) +- Test: `src/gitlab/fetchers.test.ts` + +- [ ] **Step 1: Add the `CommitData` type** + +In `src/gitlab/types.ts`, add after `IssueData`: + +```ts +export interface CommitData { + shortId: string; + title: string; + webUrl: string; + authorName: string; + createdAt: string; +} +``` + +- [ ] **Step 2: Write the failing test** + +In `src/gitlab/fetchers.test.ts`, import `fetchCommits` in the existing import from `./fetchers`. Add: + +```ts +describe("fetchCommits", () => { + it("normalizes commits and respects the limit", async () => { + const client = { + getCommits: vi.fn(async () => [ + { short_id: "a1b2c3d", title: "fix: thing", web_url: "https://gitlab.com/g/r/-/commit/a1b2c3d", + author_name: "Ada", created_at: "2026-01-02T00:00:00Z" }, + ]), + }; + const c = ctx(client); + const data = await fetchCommits(c, { project: "g/r", limit: 5 }); + expect(client.getCommits).toHaveBeenCalledWith("g/r", 5); + expect(data).toEqual([ + { shortId: "a1b2c3d", title: "fix: thing", webUrl: "https://gitlab.com/g/r/-/commit/a1b2c3d", + authorName: "Ada", createdAt: "2026-01-02T00:00:00Z" }, + ]); + }); +}); +``` + +- [ ] **Step 3: Run test to verify it fails** + +Run: `npx vitest run src/gitlab/fetchers.test.ts -t "fetchCommits"` +Expected: FAIL — `fetchCommits is not exported` / not a function. + +- [ ] **Step 4: Implement `fetchCommits`** + +In `src/gitlab/fetchers.ts`, add `CommitData` to the type import from `./types`, and add this fetcher after `fetchIssues`: + +```ts +export async function fetchCommits(ctx: GitLabContext, attrs: Attrs): Promise { + const project = String(attrs.project); + const limit = typeof attrs.limit === "number" ? attrs.limit : 10; + return memo(ctx, `commits:${project}:${limit}`, async () => { + const raw = await ctx.client.getCommits(attrs.project as string | number, limit); + return raw.map((c: any) => ({ + shortId: c.short_id, + title: c.title, + webUrl: c.web_url, + authorName: c.author_name ?? "", + createdAt: c.created_at, + } satisfies CommitData)); + }); +} +``` + +Also add `getCommits` to the `GitLabClient` type surface used by fetchers if a local interface exists — it does not; fetchers call `ctx.client` (typed `GitLabClient`), so no extra change beyond Task 1. + +- [ ] **Step 5: Run test to verify it passes** + +Run: `npx vitest run src/gitlab/fetchers.test.ts -t "fetchCommits"` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/gitlab/types.ts src/gitlab/fetchers.ts src/gitlab/fetchers.test.ts +git commit -m "feat: add CommitData type and fetchCommits fetcher" +``` + +--- + +## Task 3: Compose sections into `fetchProjectInfo` + +**Files:** +- Modify: `src/gitlab/types.ts` (extend `ProjectInfoData`) +- Modify: `src/gitlab/fetchers.ts` (`fetchProjectInfo` + layout validator) +- Test: `src/gitlab/fetchers.test.ts` + +- [ ] **Step 1: Extend `ProjectInfoData`** + +In `src/gitlab/types.ts`, add optional fields to `ProjectInfoData` (after `avatarUrl`): + +```ts + releases?: ReleaseData[]; + commits?: CommitData[]; + issues?: IssueData[]; +``` + +- [ ] **Step 2: Write the failing tests** + +In `src/gitlab/fetchers.test.ts`, add inside `describe("fetchProjectInfo", ...)`: + +```ts +it("attaches sections only when their count is > 0", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + })), + getReleases: vi.fn(async () => [ + { name: "v1", tag_name: "v1", released_at: "2026-01-01T00:00:00Z", description: "", upcoming_release: false, assets: { links: [] } }, + ]), + getCommits: vi.fn(async () => [ + { short_id: "a1", title: "t", web_url: "u", author_name: "Ada", created_at: "2026-01-02T00:00:00Z" }, + ]), + getIssues: vi.fn(async () => []), + }; + const c = ctx(client); + const data = await fetchProjectInfo(c, { project: "g/r", releases: 2, commits: 3 }); + expect(client.getReleases).toHaveBeenCalledWith("g/r", 2); + expect(client.getCommits).toHaveBeenCalledWith("g/r", 3); + expect(client.getIssues).not.toHaveBeenCalled(); + expect(data.releases).toHaveLength(1); + expect(data.commits).toHaveLength(1); + expect(data.issues).toBeUndefined(); +}); + +it("does not fetch a section when its count is 0 or absent", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + })), + getReleases: vi.fn(async () => []), + getCommits: vi.fn(async () => []), + getIssues: vi.fn(async () => []), + }; + const c = ctx(client); + const data = await fetchProjectInfo(c, { project: "g/r", commits: 0 }); + expect(client.getReleases).not.toHaveBeenCalled(); + expect(client.getCommits).not.toHaveBeenCalled(); + expect(client.getIssues).not.toHaveBeenCalled(); + expect(data.releases).toBeUndefined(); +}); + +it("omits a failing section in non-strict mode instead of throwing", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + })), + getReleases: vi.fn(async () => { throw new Error("boom"); }), + }; + const c = ctx(client); + c.options.strict = false; + const data = await fetchProjectInfo(c, { project: "g/r", releases: 2 }); + expect(data.releases).toBeUndefined(); + expect(data.name).toBe("r"); +}); + +it("rethrows a failing section in strict mode", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + })), + getReleases: vi.fn(async () => { throw new Error("boom"); }), + }; + const c = ctx(client); + c.options.strict = true; + await expect(fetchProjectInfo(c, { project: "g/r", releases: 2 })).rejects.toThrow("boom"); +}); + +it("rejects an invalid section layout", async () => { + const client = { getProject: vi.fn(async () => ({ id: 1, path_with_namespace: "g/r", name: "r", description: "", web_url: "u", star_count: 0, forks_count: 0, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null })) }; + await expect(fetchProjectInfo(ctx(client), { project: "g/r", releasesLayout: "grid" })).rejects.toThrow(/releasesLayout/); +}); +``` + +- [ ] **Step 3: Run tests to verify they fail** + +Run: `npx vitest run src/gitlab/fetchers.test.ts -t "fetchProjectInfo"` +Expected: FAIL — new assertions fail (sections not attached, layout not validated). + +- [ ] **Step 4: Implement the composition** + +In `src/gitlab/fetchers.ts`, add a section-layout validator near `readLayout`: + +```ts +function readSectionLayout(value: unknown, attr: string): "list" | "cards" { + if (value === undefined || value === "list" || value === "cards") { + return value === undefined ? "list" : value; + } + throw new Error( + `@ebuildy/docusaurus-plugin-gitlab: "${attr}" must be "list" or "cards"; ` + + `got ${JSON.stringify(value)}.`, + ); +} +``` + +Then rewrite `fetchProjectInfo` so it validates layouts up front, builds the base object inside `memo`, and attaches the count-gated sections (respecting `strict`): + +```ts +export async function fetchProjectInfo(ctx: GitLabContext, attrs: Attrs): Promise { + const project = String(attrs.project); + // Validate presentational layout literals early (values are read by the component). + readSectionLayout(attrs.releasesLayout, "releasesLayout"); + readSectionLayout(attrs.commitsLayout, "commitsLayout"); + readSectionLayout(attrs.issuesLayout, "issuesLayout"); + + const rN = typeof attrs.releases === "number" ? attrs.releases : 0; + const cN = typeof attrs.commits === "number" ? attrs.commits : 0; + const iN = typeof attrs.issues === "number" ? attrs.issues : 0; + const strict = ctx.options.strict ?? true; + + async function section(count: number, fn: () => Promise): Promise { + if (!(count > 0)) return undefined; + try { + return await fn(); + } catch (err) { + if (strict) throw err; + return undefined; + } + } + + return memo(ctx, `projectInfo:${project}:r${rN}:c${cN}:i${iN}`, async () => { + const p = await ctx.client.getProject(attrs.project as string | number); + const avatarUrl = p.avatar_url ? await ctx.assets.localize(p.avatar_url, "", project) : null; + const [releases, commits, issues] = await Promise.all([ + section(rN, () => fetchReleases(ctx, { project, limit: rN })), + section(cN, () => fetchCommits(ctx, { project, limit: cN })), + section(iN, () => fetchIssues(ctx, { project, limit: iN })), + ]); + const base: ProjectInfoData = { + id: p.id, + path: p.path_with_namespace, + name: p.name, + descriptionHtml: await renderMarkdown(p.description ?? "", { renderChain: ctx.options.markdownRenderChain }), + webUrl: p.web_url, + starCount: p.star_count, + forksCount: p.forks_count, + topics: p.topics ?? [], + lastActivityAt: p.last_activity_at, + avatarUrl, + }; + if (releases) base.releases = releases; + if (commits) base.commits = commits; + if (issues) base.issues = issues; + return base; + }).then((v) => ({ ...v, path: v.path || project })); +} +``` + +Note: `fetchReleases`/`fetchIssues`/`fetchCommits` are hoisted function declarations in the same module, so the forward references are fine. + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `npx vitest run src/gitlab/fetchers.test.ts -t "fetchProjectInfo"` +Expected: PASS. Then run the full fetchers file to catch regressions: `npx vitest run src/gitlab/fetchers.test.ts` → PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/gitlab/types.ts src/gitlab/fetchers.ts src/gitlab/fetchers.test.ts +git commit -m "feat: compose count-gated sections into fetchProjectInfo" +``` + +--- + +## Task 4: Render compact sections + `link` override in the component + +**Files:** +- Modify: `src/components/GitlabProjectInfo.tsx` +- Test: `src/components/GitlabProjectInfo.test.tsx` + +- [ ] **Step 1: Write the failing tests** + +In `src/components/GitlabProjectInfo.test.tsx`, add: + +```ts +it("renders compact release, commit, and issue lines after the description", () => { + render(); + expect(screen.getByText("First")).toBeInTheDocument(); + expect(screen.getByText("v1.0")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "a1b2c3d" })).toHaveAttribute("href", "https://gitlab.com/c/a1b2c3d"); + expect(screen.getByText("fix: bug")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: /Broken thing/ })).toHaveAttribute("href", "https://gitlab.com/i/42"); +}); + +it("renders no section blocks when arrays are absent", () => { + const { container } = render(); + expect(container.querySelector(".gitlab-section")).toBeNull(); +}); + +it("overrides the title link when link is provided", () => { + render(); + expect(screen.getByRole("link", { name: "My Repo" })).toHaveAttribute("href", "https://example.com/app"); +}); + +it("defaults the title link to the project webUrl", () => { + render(); + expect(screen.getByRole("link", { name: "My Repo" })).toHaveAttribute("href", "https://gitlab.com/g/r"); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npx vitest run src/components/GitlabProjectInfo.test.tsx` +Expected: FAIL — sections not rendered, `link` not applied. + +- [ ] **Step 3: Implement sections + link override** + +Rewrite `src/components/GitlabProjectInfo.tsx`. Update the imports and props, add a `SectionLayout` type + three local render helpers, use `link` for the title href, and render the sections right after the description block: + +```tsx +import React from "react"; +import { Fallback } from "./Fallback.js"; +import { formatCount } from "./format.js"; +import type { ComponentPayload, ProjectInfoData, ReleaseData, CommitData, IssueData } from "./types.js"; + +type SectionLayout = "list" | "cards"; + +interface ProjectInfoProps extends ComponentPayload { + showStats?: boolean; + link?: string; + releasesLayout?: SectionLayout; + commitsLayout?: SectionLayout; + issuesLayout?: SectionLayout; +} + +function shortDate(iso: string): string { + return new Date(iso).toLocaleDateString(); +} + +function Releases({ items, layout }: { items: ReleaseData[]; layout: SectionLayout }) { + return ( +
+
Releases
+
    + {items.map((r) => ( +
  • + {r.tagName} + {r.name || r.tagName} + {layout === "cards" && ( + · {shortDate(r.releasedAt)} + )} +
  • + ))} +
+
+ ); +} + +function Commits({ items, layout }: { items: CommitData[]; layout: SectionLayout }) { + return ( +
+
Latest commits
+
    + {items.map((c) => ( +
  • + {c.shortId} + {c.title} + · {c.authorName} · {shortDate(c.createdAt)} +
  • + ))} +
+
+ ); +} + +function Issues({ items, layout }: { items: IssueData[]; layout: SectionLayout }) { + return ( +
+
Issues
+
    + {items.map((i) => ( +
  • + #{i.iid} {i.title} + {layout === "cards" && ( + · {i.state} · {i.authorName} + )} +
  • + ))} +
+
+ ); +} + +export function GitlabProjectInfo({ + data, + error, + showStats = true, + link, + releasesLayout = "list", + commitsLayout = "list", + issuesLayout = "list", +}: ProjectInfoProps) { + if (error) return ; + if (!data) return null; + return ( +
+
+ {data.avatarUrl && ( + {data.name} + )} + +
+ {data.descriptionHtml && ( +
+ )} + {data.releases && data.releases.length > 0 && ( + + )} + {data.commits && data.commits.length > 0 && ( + + )} + {data.issues && data.issues.length > 0 && ( + + )} + {data.topics.length > 0 && ( +
+ {data.topics.map((t) => ( + {t} + ))} +
+ )} + {showStats && ( +
+ ★ {formatCount(data.starCount)} + ⑂ {formatCount(data.forksCount)} + updated {new Date(data.lastActivityAt).toLocaleDateString()} +
+ )} +
+ ); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npx vitest run src/components/GitlabProjectInfo.test.tsx` +Expected: PASS (all existing + new tests). + +- [ ] **Step 5: Commit** + +```bash +git add src/components/GitlabProjectInfo.tsx src/components/GitlabProjectInfo.test.tsx +git commit -m "feat: render embedded sections and link override in GitlabProjectInfo" +``` + +--- + +## Task 5: `cards` layout coverage + export `CommitData` + +**Files:** +- Modify: `src/components/types.ts`, `src/components/index.ts`, `src/index.ts` +- Test: `src/components/GitlabProjectInfo.test.tsx` + +- [ ] **Step 1: Write the failing test (cards layout)** + +In `src/components/GitlabProjectInfo.test.tsx`, add: + +```ts +it("shows richer metadata in cards layout", () => { + render(); + expect(screen.getByText(/opened/)).toBeInTheDocument(); + expect(screen.getByText(/Ada/)).toBeInTheDocument(); +}); +``` + +- [ ] **Step 2: Run test to verify it fails, then passes** + +Run: `npx vitest run src/components/GitlabProjectInfo.test.tsx -t "cards layout"` +The rendering from Task 4 already implements the `cards` branch, so this test should PASS immediately. If it FAILS, fix the `layout === "cards"` branch in the `Issues` helper. (This task exists to lock in cards behavior and finish exports.) + +- [ ] **Step 3: Re-export `CommitData`** + +Add `CommitData` to the export list in `src/components/types.ts`: + +```ts +export type { + ProjectInfoData, + ReleaseData, + IssueData, + CommitData, + ReadmeData, + FileData, + TopicData, + LabelData, + FetchError, + ComponentPayload, +} from "../gitlab/types.js"; +``` + +Add `CommitData` to the `export type { … } from "./types.js";` block in `src/components/index.ts` (insert after `IssueData`). + +Add `CommitData` to the `export type { … }` block in `src/index.ts` (insert after `IssueData`). + +- [ ] **Step 4: Typecheck + run tests** + +Run: `npm run typecheck` +Expected: no errors. +Run: `npx vitest run src/components/GitlabProjectInfo.test.tsx src/gitlab/fetchers.test.ts src/gitlab/client.test.ts` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/components/types.ts src/components/index.ts src/index.ts src/components/GitlabProjectInfo.test.tsx +git commit -m "feat: export CommitData and cover cards layout" +``` + +--- + +## Task 6: Documentation (Feature 1) + +**Files:** +- Modify: `README.md` +- Modify: `examples/site/docs/components/` (the `GitlabProjectInfo` page) + +- [ ] **Step 1: Locate the docs** + +Run: `grep -rl "GitlabProjectInfo" README.md examples/site/docs` +Open the README section and the example page for `GitlabProjectInfo`. + +- [ ] **Step 2: Document the new attributes** + +Add an attributes subsection describing: `releases={N}`, `commits={N}`, `issues={N}` (opt-in counts; no fetch when unset/≤0), `releasesLayout` / `commitsLayout` / `issuesLayout` (`"list"` default, `"cards"`), and `link` (overrides the title href, defaults to the project URL). Include a worked example: + +```mdx + + +``` + +- [ ] **Step 3: Commit** + +```bash +git add README.md examples/site/docs +git commit -m "docs: document GitlabProjectInfo embedded sections" +``` + +--- + +# FEATURE 2 — Extended project stats + +## File structure (Feature 2) + +- Create `src/components/formatBytes.ts` — new `formatBytes` helper (kept next to `format.ts`). +- Modify `src/gitlab/client.ts` — `getProject` statistics option + `getContributorsCount`. +- Modify `src/gitlab/types.ts` — add stat fields to `ProjectInfoData`. +- Modify `src/gitlab/fetchers.ts` — map stats in `fetchProjectInfo`. +- Modify `src/components/GitlabProjectInfo.tsx` — render stat pills. +- Tests: `src/components/formatBytes.test.ts`, `src/gitlab/client.test.ts`, `src/gitlab/fetchers.test.ts`, `src/components/GitlabProjectInfo.test.tsx`. +- Docs: `README.md`, example page. + +--- + +## Task 7: `formatBytes` helper + +**Files:** +- Create: `src/components/formatBytes.ts` +- Test: `src/components/formatBytes.test.ts` + +- [ ] **Step 1: Write the failing test** + +Create `src/components/formatBytes.test.ts`: + +```ts +import { describe, it, expect } from "vitest"; +import { formatBytes } from "./formatBytes.js"; + +describe("formatBytes", () => { + it("formats zero and bytes", () => { + expect(formatBytes(0)).toBe("0 B"); + expect(formatBytes(512)).toBe("512 B"); + }); + it("formats KB, MB, GB with one decimal", () => { + expect(formatBytes(1024)).toBe("1 KB"); + expect(formatBytes(1536)).toBe("1.5 KB"); + expect(formatBytes(4_404_019)).toBe("4.2 MB"); + expect(formatBytes(2_147_483_648)).toBe("2 GB"); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `npx vitest run src/components/formatBytes.test.ts` +Expected: FAIL — module not found. + +- [ ] **Step 3: Implement `formatBytes`** + +Create `src/components/formatBytes.ts`: + +```ts +/** Humanize a byte count: 1536 -> "1.5 KB", 4.4e6 -> "4.2 MB". */ +export function formatBytes(n: number): string { + if (n < 1024) return `${n} B`; + const units = ["KB", "MB", "GB", "TB"]; + let value = n; + let unit = -1; + do { + value /= 1024; + unit += 1; + } while (value >= 1024 && unit < units.length - 1); + return `${parseFloat(value.toFixed(1))} ${units[unit]}`; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `npx vitest run src/components/formatBytes.test.ts` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/components/formatBytes.ts src/components/formatBytes.test.ts +git commit -m "feat: add formatBytes helper" +``` + +--- + +## Task 8: `getProject` statistics option + `getContributorsCount` + +**Files:** +- Modify: `src/gitlab/client.ts` +- Test: `src/gitlab/client.test.ts` + +- [ ] **Step 1: Write the failing tests** + +In `src/gitlab/client.test.ts`, add a `contributorsAllMock = vi.fn();` at the top, wire it into the mocked Gitlab object as `Repositories: { allContributors: contributorsAllMock }`, and reset it in `beforeEach`. Add: + +```ts +it("getProject forwards the statistics option", async () => { + showMock.mockResolvedValue({ id: 1 }); + const client = new GitLabClient({ host: "https://gitlab.com" }); + await client.getProject("g/r", { statistics: true }); + expect(showMock).toHaveBeenCalledWith("g/r", { statistics: true }); +}); + +it("getProject omits options by default", async () => { + showMock.mockResolvedValue({ id: 1 }); + const client = new GitLabClient({ host: "https://gitlab.com" }); + await client.getProject("g/r"); + expect(showMock).toHaveBeenCalledWith("g/r"); +}); + +it("getContributorsCount returns the pagination total", async () => { + contributorsAllMock.mockResolvedValue({ data: [{}], paginationInfo: { total: 8 } }); + const client = new GitLabClient({ host: "https://gitlab.com" }); + const count = await client.getContributorsCount("g/r"); + expect(contributorsAllMock).toHaveBeenCalledWith("g/r", { showExpanded: true, perPage: 1, maxPages: 1 }); + expect(count).toBe(8); +}); + +it("getContributorsCount returns undefined when total is absent", async () => { + contributorsAllMock.mockResolvedValue({ data: [], paginationInfo: {} }); + const client = new GitLabClient({ host: "https://gitlab.com" }); + expect(await client.getContributorsCount("g/r")).toBeUndefined(); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npx vitest run src/gitlab/client.test.ts -t "getProject forwards|getContributorsCount"` +Expected: FAIL. + +- [ ] **Step 3: Implement** + +In `src/gitlab/client.ts`, change `getProject` to accept options and add `getContributorsCount`: + +```ts + async getProject(project: ProjectRef, opts?: { statistics?: boolean }): Promise { + return opts ? this.api.Projects.show(project, opts) : this.api.Projects.show(project); + } + + async getContributorsCount(project: ProjectRef): Promise { + const res: any = await this.api.Repositories.allContributors(project, { + showExpanded: true, + perPage: 1, + maxPages: 1, + }); + const total = res?.paginationInfo?.total; + return typeof total === "number" ? total : undefined; + } +``` + +Note: passing `undefined` options to `Projects.show` would change the call signature in the "omits options by default" test — that is why the ternary calls `show(project)` with no second arg when `opts` is undefined. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npx vitest run src/gitlab/client.test.ts` +Expected: PASS (all client tests — the existing `getProject` callers still pass one arg). + +- [ ] **Step 5: Commit** + +```bash +git add src/gitlab/client.ts src/gitlab/client.test.ts +git commit -m "feat: add statistics option and getContributorsCount to client" +``` + +--- + +## Task 9: Map stats in `fetchProjectInfo` + +**Files:** +- Modify: `src/gitlab/types.ts` (stat fields) +- Modify: `src/gitlab/fetchers.ts` +- Test: `src/gitlab/fetchers.test.ts` + +- [ ] **Step 1: Extend `ProjectInfoData`** + +In `src/gitlab/types.ts`, add to `ProjectInfoData` (after the section fields from Task 3): + +```ts + openIssuesCount?: number; + commitCount?: number; + repositorySize?: number; + contributorsCount?: number; +``` + +- [ ] **Step 2: Write the failing tests** + +In `src/gitlab/fetchers.test.ts`, add inside `describe("fetchProjectInfo", ...)`: + +```ts +it("maps statistics, open issues, and contributors count", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + issues_enabled: true, open_issues_count: 12, + statistics: { commit_count: 1200, repository_size: 4404019 }, + })), + getContributorsCount: vi.fn(async () => 8), + }; + const c = ctx(client); + const data = await fetchProjectInfo(c, { project: "g/r" }); + expect(client.getProject).toHaveBeenCalledWith("g/r", { statistics: true }); + expect(data.commitCount).toBe(1200); + expect(data.repositorySize).toBe(4404019); + expect(data.openIssuesCount).toBe(12); + expect(data.contributorsCount).toBe(8); +}); + +it("omits statistics-derived stats when statistics is absent", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + issues_enabled: false, + })), + getContributorsCount: vi.fn(async () => undefined), + }; + const c = ctx(client); + const data = await fetchProjectInfo(c, { project: "g/r" }); + expect(data.commitCount).toBeUndefined(); + expect(data.repositorySize).toBeUndefined(); + expect(data.openIssuesCount).toBeUndefined(); + expect(data.contributorsCount).toBeUndefined(); +}); + +it("never throws when the contributors fetch fails, even in strict mode", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + issues_enabled: true, open_issues_count: 5, statistics: { commit_count: 1, repository_size: 1 }, + })), + getContributorsCount: vi.fn(async () => { throw new Error("no perms"); }), + }; + const c = ctx(client); + c.options.strict = true; + const data = await fetchProjectInfo(c, { project: "g/r" }); + expect(data.contributorsCount).toBeUndefined(); + expect(data.commitCount).toBe(1); +}); +``` + +Also update the two existing `fetchProjectInfo` tests that assert `expect(client.getProject).toHaveBeenCalledWith("g/r")` — they must now expect `toHaveBeenCalledWith("g/r", { statistics: true })`. Those two fakes have no `getContributorsCount`; add `getContributorsCount: vi.fn(async () => undefined)` to their `client` objects. + +- [ ] **Step 3: Run tests to verify they fail** + +Run: `npx vitest run src/gitlab/fetchers.test.ts -t "fetchProjectInfo"` +Expected: FAIL — stats not mapped; `getProject` called without options. + +- [ ] **Step 4: Implement stat mapping** + +In `src/gitlab/fetchers.ts`, inside the `fetchProjectInfo` memo closure, change the project fetch to request statistics and add the stat mapping. Update the `getProject` call: + +```ts + const p = await ctx.client.getProject(attrs.project as string | number, { statistics: true }); +``` + +Compute the contributors count best-effort (never throws) alongside the section fetches: + +```ts + const contributorsCount = await ctx.client + .getContributorsCount(attrs.project as string | number) + .catch(() => undefined); +``` + +After building `base` (before the `if (releases)` lines), attach the stats: + +```ts + if (typeof p.statistics?.commit_count === "number") base.commitCount = p.statistics.commit_count; + if (typeof p.statistics?.repository_size === "number") base.repositorySize = p.statistics.repository_size; + if (p.issues_enabled && typeof p.open_issues_count === "number") base.openIssuesCount = p.open_issues_count; + if (typeof contributorsCount === "number") base.contributorsCount = contributorsCount; +``` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `npx vitest run src/gitlab/fetchers.test.ts` +Expected: PASS (including the two updated existing tests). + +- [ ] **Step 6: Commit** + +```bash +git add src/gitlab/types.ts src/gitlab/fetchers.ts src/gitlab/fetchers.test.ts +git commit -m "feat: map commit/contributor/issue/size stats in fetchProjectInfo" +``` + +--- + +## Task 10: Render stat pills in the component + +**Files:** +- Modify: `src/components/GitlabProjectInfo.tsx` +- Test: `src/components/GitlabProjectInfo.test.tsx` + +- [ ] **Step 1: Write the failing tests** + +In `src/components/GitlabProjectInfo.test.tsx`, add: + +```ts +it("appends stat pills when their data is present", () => { + render(); + expect(screen.getByText(/1.2k commits/)).toBeInTheDocument(); + expect(screen.getByText(/8 contributors/)).toBeInTheDocument(); + expect(screen.getByText(/12 issues/)).toBeInTheDocument(); + expect(screen.getByText(/4.2 MB/)).toBeInTheDocument(); +}); + +it("omits stat pills whose data is absent", () => { + render(); + expect(screen.queryByText(/commits/)).not.toBeInTheDocument(); + expect(screen.queryByText(/contributors/)).not.toBeInTheDocument(); +}); + +it("hides all stats including new pills when showStats is false", () => { + render(); + expect(screen.queryByText(/commits/)).not.toBeInTheDocument(); + expect(screen.queryByText(/★/)).not.toBeInTheDocument(); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npx vitest run src/components/GitlabProjectInfo.test.tsx -t "stat pills|showStats is false"` +Expected: FAIL. + +- [ ] **Step 3: Implement the pills** + +In `src/components/GitlabProjectInfo.tsx`, add the `formatBytes` import: + +```tsx +import { formatBytes } from "./formatBytes.js"; +``` + +Inside the `showStats` block, add the new pills after the forks span and before the `updated` span: + +```tsx + ⑂ {formatCount(data.forksCount)} + {typeof data.commitCount === "number" && ( + ⎇ {formatCount(data.commitCount)} commits + )} + {typeof data.contributorsCount === "number" && ( + 👥 {formatCount(data.contributorsCount)} contributors + )} + {typeof data.openIssuesCount === "number" && ( + ⊙ {formatCount(data.openIssuesCount)} issues + )} + {typeof data.repositorySize === "number" && ( + ▤ {formatBytes(data.repositorySize)} + )} + updated {new Date(data.lastActivityAt).toLocaleDateString()} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npx vitest run src/components/GitlabProjectInfo.test.tsx` +Expected: PASS. + +- [ ] **Step 5: Typecheck** + +Run: `npm run typecheck` +Expected: no errors. + +- [ ] **Step 6: Commit** + +```bash +git add src/components/GitlabProjectInfo.tsx src/components/GitlabProjectInfo.test.tsx +git commit -m "feat: render extended stat pills in GitlabProjectInfo" +``` + +--- + +## Task 11: Documentation (Feature 2) + full verification + +**Files:** +- Modify: `README.md`, example page +- Verify: whole test suite + typecheck + +- [ ] **Step 1: Document the stats** + +In the README and the `GitlabProjectInfo` example page, describe the new stat pills (commits, contributors, open issues, repository size), note they appear automatically when data is available and are still gated by `showStats`, and add the **Reporter+ token** caveat: commits count and repository size require the build-time token to have Reporter access; otherwise those two pills are omitted. + +- [ ] **Step 2: Run the full unit suite** + +Run: `npx vitest run` +Expected: PASS (all files). + +- [ ] **Step 3: Typecheck + build** + +Run: `npm run typecheck && npm run build` +Expected: no errors; `dist/` emitted. + +- [ ] **Step 4: Commit** + +```bash +git add README.md examples/site/docs +git commit -m "docs: document GitlabProjectInfo extended stats" +``` + +- [ ] **Step 5 (optional but recommended): e2e** + +If you touched anything the e2e site exercises, add a `` usage to an example page and run: +Run: `npx vitest run test/e2e/build.test.ts` +Expected: PASS (slow, ~1 min). + +--- + +## Self-review notes (already reconciled) + +- **Spec coverage:** sections (Tasks 1–4), layouts + exports (Task 5), sections docs (Task 6); stats helper (Task 7), client (Task 8), fetcher mapping (Task 9), component pills (Task 10), stats docs + verification (Task 11). Code-lines is intentionally absent (no API). +- **Strict semantics:** section fetches respect `strict` (Task 3); contributors/statistics are best-effort and never throw (Tasks 8–9). +- **Type consistency:** `CommitData` (`shortId/title/webUrl/authorName/createdAt`) is defined in Task 2 and used identically in Tasks 3–5; `getProject(project, { statistics })`, `getContributorsCount`, and the `openIssuesCount/commitCount/repositorySize/contributorsCount` fields are named identically across Tasks 8–10. +- **Existing-test updates:** Task 9 Step 2 explicitly updates the two prior `fetchProjectInfo` tests for the new `getProject` signature. From ff972f80a0b3ee3c2f04beb6664053329c8f232b Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 08:49:56 +0200 Subject: [PATCH 04/28] feat: add getCommits to GitLabClient Signed-off-by: Thomas Decaux --- src/gitlab/client.test.ts | 16 ++++++++++++++++ src/gitlab/client.ts | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/src/gitlab/client.test.ts b/src/gitlab/client.test.ts index c575a6e..607bc40 100644 --- a/src/gitlab/client.test.ts +++ b/src/gitlab/client.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; const showMock = vi.fn(); const releasesAllMock = vi.fn(); const issuesAllMock = vi.fn(); +const commitsAllMock = vi.fn(); const showRawMock = vi.fn(); const gitlabCtor = vi.fn(); const topicsAllMock = vi.fn(); @@ -19,6 +20,7 @@ vi.mock("@gitbeaker/rest", () => ({ Projects: { show: showMock }, ProjectReleases: { all: releasesAllMock }, Issues: { all: issuesAllMock }, + Commits: { all: commitsAllMock }, RepositoryFiles: { showRaw: showRawMock }, Topics: { all: topicsAllMock }, ProjectLabels: { all: projectLabelsAllMock }, @@ -37,6 +39,7 @@ beforeEach(() => { showMock.mockReset(); releasesAllMock.mockReset(); issuesAllMock.mockReset(); + commitsAllMock.mockReset(); showRawMock.mockReset(); gitlabCtor.mockReset(); topicsAllMock.mockReset(); @@ -90,6 +93,19 @@ describe("GitLabClient", () => { expect(data).toEqual([{ iid: 1 }, { iid: 2 }]); }); + it("getCommits fetches one page and slices to the limit", async () => { + commitsAllMock.mockResolvedValue([ + { short_id: "a1", title: "one" }, + { short_id: "b2", title: "two" }, + { short_id: "c3", title: "three" }, + ]); + const client = new GitLabClient({ host: "https://gitlab.com" }); + const commits = await client.getCommits("g/r", 2); + expect(commitsAllMock).toHaveBeenCalledWith("g/r", { perPage: 2, maxPages: 1 }); + expect(commits).toHaveLength(2); + expect(commits[0].short_id).toBe("a1"); + }); + it("getFileRaw delegates to RepositoryFiles.showRaw with the given path and ref", async () => { showRawMock.mockResolvedValue("# hello"); const c = new GitLabClient({ host: "https://gitlab.com" }); diff --git a/src/gitlab/client.ts b/src/gitlab/client.ts index 2d72720..35537d6 100644 --- a/src/gitlab/client.ts +++ b/src/gitlab/client.ts @@ -57,6 +57,11 @@ export class GitLabClient { return issues.slice(0, opts.limit); } + async getCommits(project: ProjectRef, limit: number): Promise { + const commits = await this.api.Commits.all(project, { perPage: limit, maxPages: 1 }); + return commits.slice(0, limit); + } + async getFileRaw(project: ProjectRef, path: string, ref: string): Promise { const raw = await this.api.RepositoryFiles.showRaw(project, path, ref); return typeof raw === "string" ? raw : await raw.text(); From 865a79192af8dd5ea1ce3a3afba85b21493968b2 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 08:53:05 +0200 Subject: [PATCH 05/28] feat: add CommitData type and fetchCommits fetcher Signed-off-by: Thomas Decaux --- src/gitlab/fetchers.test.ts | 20 +++++++++++++++++++- src/gitlab/fetchers.ts | 16 ++++++++++++++++ src/gitlab/types.ts | 8 ++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/gitlab/fetchers.test.ts b/src/gitlab/fetchers.test.ts index 801dc15..b4c43ff 100644 --- a/src/gitlab/fetchers.test.ts +++ b/src/gitlab/fetchers.test.ts @@ -6,7 +6,7 @@ import remarkParse from "remark-parse"; import remarkRehype from "remark-rehype"; import { describe, it, expect, vi } from "vitest"; import { FileCache } from "./cache"; -import { fetchProjectInfo, fetchReleases, fetchIssues, fetchReadme, fetchFile, fetchTopics, fetchLabels } from "./fetchers"; +import { fetchProjectInfo, fetchReleases, fetchIssues, fetchCommits, fetchReadme, fetchFile, fetchTopics, fetchLabels } from "./fetchers"; function ctx(client: any) { const dir = mkdtempSync(join(tmpdir(), "glfetch-")); @@ -109,6 +109,24 @@ describe("fetchIssues", () => { }); }); +describe("fetchCommits", () => { + it("normalizes commits and respects the limit", async () => { + const client = { + getCommits: vi.fn(async () => [ + { short_id: "a1b2c3d", title: "fix: thing", web_url: "https://gitlab.com/g/r/-/commit/a1b2c3d", + author_name: "Ada", created_at: "2026-01-02T00:00:00Z" }, + ]), + }; + const c = ctx(client); + const data = await fetchCommits(c, { project: "g/r", limit: 5 }); + expect(client.getCommits).toHaveBeenCalledWith("g/r", 5); + expect(data).toEqual([ + { shortId: "a1b2c3d", title: "fix: thing", webUrl: "https://gitlab.com/g/r/-/commit/a1b2c3d", + authorName: "Ada", createdAt: "2026-01-02T00:00:00Z" }, + ]); + }); +}); + describe("fetchReadme", () => { it("resolves default branch, renders html, and localizes images", async () => { const client: any = { diff --git a/src/gitlab/fetchers.ts b/src/gitlab/fetchers.ts index 656ff2e..15d9fc2 100644 --- a/src/gitlab/fetchers.ts +++ b/src/gitlab/fetchers.ts @@ -10,6 +10,7 @@ import type { GitLabClient, PageOptions } from "./client"; import { renderMarkdown } from "./markdown.js"; import type { TocEntry, TocMode } from "./toc.js"; import type { + CommitData, FileData, IssueData, LabelData, @@ -135,6 +136,21 @@ export async function fetchIssues(ctx: GitLabContext, attrs: Attrs): Promise { + const project = String(attrs.project); + const limit = typeof attrs.limit === "number" ? attrs.limit : 10; + return memo(ctx, `commits:${project}:${limit}`, async () => { + const raw = await ctx.client.getCommits(attrs.project as string | number, limit); + return raw.map((c: any) => ({ + shortId: c.short_id, + title: c.title, + webUrl: c.web_url, + authorName: c.author_name ?? "", + createdAt: c.created_at, + } satisfies CommitData)); + }); +} + interface OrderSpec { field: "name"; dir: "asc" | "desc"; diff --git a/src/gitlab/types.ts b/src/gitlab/types.ts index 088d52b..1cc84b4 100644 --- a/src/gitlab/types.ts +++ b/src/gitlab/types.ts @@ -41,6 +41,14 @@ export interface IssueData { createdAt: string; } +export interface CommitData { + shortId: string; + title: string; + webUrl: string; + authorName: string; + createdAt: string; +} + export interface ReadmeData { ref: string; html: string; From 6fa40986cf1a40686cb3221cc272631017d61e6f Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 08:57:24 +0200 Subject: [PATCH 06/28] feat: compose count-gated sections into fetchProjectInfo Signed-off-by: Thomas Decaux --- src/gitlab/fetchers.test.ts | 75 +++++++++++++++++++++++++++++++++++++ src/gitlab/fetchers.ts | 49 +++++++++++++++++++++--- src/gitlab/types.ts | 3 ++ 3 files changed, 121 insertions(+), 6 deletions(-) diff --git a/src/gitlab/fetchers.test.ts b/src/gitlab/fetchers.test.ts index b4c43ff..88bc99f 100644 --- a/src/gitlab/fetchers.test.ts +++ b/src/gitlab/fetchers.test.ts @@ -49,6 +49,81 @@ describe("fetchProjectInfo", () => { expect(c.assets.localize).toHaveBeenCalledWith("https://gitlab.com/uploads/avatar.png", "", "g/r"); expect(data.avatarUrl).toBe("/gitlab-assets/httpsgitlabcomuploadsavatarpng.png"); }); + + it("attaches sections only when their count is > 0", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + })), + getReleases: vi.fn(async () => [ + { name: "v1", tag_name: "v1", released_at: "2026-01-01T00:00:00Z", description: "", upcoming_release: false, assets: { links: [] } }, + ]), + getCommits: vi.fn(async () => [ + { short_id: "a1", title: "t", web_url: "u", author_name: "Ada", created_at: "2026-01-02T00:00:00Z" }, + ]), + getIssues: vi.fn(async () => []), + }; + const c = ctx(client); + const data = await fetchProjectInfo(c, { project: "g/r", releases: 2, commits: 3 }); + expect(client.getReleases).toHaveBeenCalledWith("g/r", 2); + expect(client.getCommits).toHaveBeenCalledWith("g/r", 3); + expect(client.getIssues).not.toHaveBeenCalled(); + expect(data.releases).toHaveLength(1); + expect(data.commits).toHaveLength(1); + expect(data.issues).toBeUndefined(); + }); + + it("does not fetch a section when its count is 0 or absent", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + })), + getReleases: vi.fn(async () => []), + getCommits: vi.fn(async () => []), + getIssues: vi.fn(async () => []), + }; + const c = ctx(client); + const data = await fetchProjectInfo(c, { project: "g/r", commits: 0 }); + expect(client.getReleases).not.toHaveBeenCalled(); + expect(client.getCommits).not.toHaveBeenCalled(); + expect(client.getIssues).not.toHaveBeenCalled(); + expect(data.releases).toBeUndefined(); + }); + + it("omits a failing section in non-strict mode instead of throwing", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + })), + getReleases: vi.fn(async () => { throw new Error("boom"); }), + }; + const c = ctx(client); + c.options.strict = false; + const data = await fetchProjectInfo(c, { project: "g/r", releases: 2 }); + expect(data.releases).toBeUndefined(); + expect(data.name).toBe("r"); + }); + + it("rethrows a failing section in strict mode", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + })), + getReleases: vi.fn(async () => { throw new Error("boom"); }), + }; + const c = ctx(client); + c.options.strict = true; + await expect(fetchProjectInfo(c, { project: "g/r", releases: 2 })).rejects.toThrow("boom"); + }); + + it("rejects an invalid section layout", async () => { + const client = { getProject: vi.fn(async () => ({ id: 1, path_with_namespace: "g/r", name: "r", description: "", web_url: "u", star_count: 0, forks_count: 0, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null })) }; + await expect(fetchProjectInfo(ctx(client), { project: "g/r", releasesLayout: "grid" })).rejects.toThrow(/releasesLayout/); + }); }); describe("fetchReleases", () => { diff --git a/src/gitlab/fetchers.ts b/src/gitlab/fetchers.ts index 15d9fc2..fa5bb75 100644 --- a/src/gitlab/fetchers.ts +++ b/src/gitlab/fetchers.ts @@ -71,14 +71,47 @@ async function memo(ctx: GitLabContext, key: string, fn: () => Promise): P return value; } +function readSectionLayout(value: unknown, attr: string): "list" | "cards" { + if (value === undefined || value === "list" || value === "cards") { + return value === undefined ? "list" : value; + } + throw new Error( + `@ebuildy/docusaurus-plugin-gitlab: "${attr}" must be "list" or "cards"; ` + + `got ${JSON.stringify(value)}.`, + ); +} + export async function fetchProjectInfo(ctx: GitLabContext, attrs: Attrs): Promise { const project = String(attrs.project); - return memo(ctx, `projectInfo:${project}`, async () => { + // Validate presentational layout literals early (values are read by the component). + readSectionLayout(attrs.releasesLayout, "releasesLayout"); + readSectionLayout(attrs.commitsLayout, "commitsLayout"); + readSectionLayout(attrs.issuesLayout, "issuesLayout"); + + const rN = typeof attrs.releases === "number" ? attrs.releases : 0; + const cN = typeof attrs.commits === "number" ? attrs.commits : 0; + const iN = typeof attrs.issues === "number" ? attrs.issues : 0; + const strict = ctx.options.strict ?? true; + + async function section(count: number, fn: () => Promise): Promise { + if (!(count > 0)) return undefined; + try { + return await fn(); + } catch (err) { + if (strict) throw err; + return undefined; + } + } + + return memo(ctx, `projectInfo:${project}:r${rN}:c${cN}:i${iN}`, async () => { const p = await ctx.client.getProject(attrs.project as string | number); - const avatarUrl = p.avatar_url - ? await ctx.assets.localize(p.avatar_url, "", project) - : null; - return { + const avatarUrl = p.avatar_url ? await ctx.assets.localize(p.avatar_url, "", project) : null; + const [releases, commits, issues] = await Promise.all([ + section(rN, () => fetchReleases(ctx, { project, limit: rN })), + section(cN, () => fetchCommits(ctx, { project, limit: cN })), + section(iN, () => fetchIssues(ctx, { project, limit: iN })), + ]); + const base: ProjectInfoData = { id: p.id, path: p.path_with_namespace, name: p.name, @@ -89,7 +122,11 @@ export async function fetchProjectInfo(ctx: GitLabContext, attrs: Attrs): Promis topics: p.topics ?? [], lastActivityAt: p.last_activity_at, avatarUrl, - } satisfies ProjectInfoData; + }; + if (releases) base.releases = releases; + if (commits) base.commits = commits; + if (issues) base.issues = issues; + return base; }).then((v) => ({ ...v, path: v.path || project })); } diff --git a/src/gitlab/types.ts b/src/gitlab/types.ts index 1cc84b4..c122b83 100644 --- a/src/gitlab/types.ts +++ b/src/gitlab/types.ts @@ -14,6 +14,9 @@ export interface ProjectInfoData { topics: string[]; lastActivityAt: string; avatarUrl: string | null; + releases?: ReleaseData[]; + commits?: CommitData[]; + issues?: IssueData[]; } export interface ReleaseAsset { From dd5db250d8e5ca0297886fe09ae1a5f2a4fefee1 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 09:31:18 +0200 Subject: [PATCH 07/28] test: verify projectInfo cache key varies by section counts Signed-off-by: Thomas Decaux --- src/gitlab/fetchers.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/gitlab/fetchers.test.ts b/src/gitlab/fetchers.test.ts index 88bc99f..c5ce477 100644 --- a/src/gitlab/fetchers.test.ts +++ b/src/gitlab/fetchers.test.ts @@ -124,6 +124,29 @@ describe("fetchProjectInfo", () => { const client = { getProject: vi.fn(async () => ({ id: 1, path_with_namespace: "g/r", name: "r", description: "", web_url: "u", star_count: 0, forks_count: 0, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null })) }; await expect(fetchProjectInfo(ctx(client), { project: "g/r", releasesLayout: "grid" })).rejects.toThrow(/releasesLayout/); }); + + it("caches sections separately per count (cache key varies)", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + })), + getReleases: vi.fn(async (_p: unknown, limit: number) => + Array.from({ length: limit }, (_, i) => ({ + name: `v${i}`, tag_name: `v${i}`, released_at: "2026-01-01T00:00:00Z", + description: "", upcoming_release: false, assets: { links: [] }, + })), + ), + getCommits: vi.fn(async () => []), + getIssues: vi.fn(async () => []), + }; + const c = ctx(client); + const first = await fetchProjectInfo(c, { project: "g/r", releases: 1 }); + const second = await fetchProjectInfo(c, { project: "g/r", releases: 2 }); + expect(first.releases).toHaveLength(1); + expect(second.releases).toHaveLength(2); + expect(client.getReleases).toHaveBeenCalledTimes(2); + }); }); describe("fetchReleases", () => { From 9add50a734c28836ace947139ba96479bedfbee5 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 09:33:30 +0200 Subject: [PATCH 08/28] feat: render embedded sections and link override in GitlabProjectInfo Signed-off-by: Thomas Decaux --- src/components/GitlabProjectInfo.test.tsx | 28 +++++++ src/components/GitlabProjectInfo.tsx | 99 ++++++++++++++++++++--- src/components/types.ts | 1 + 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/src/components/GitlabProjectInfo.test.tsx b/src/components/GitlabProjectInfo.test.tsx index 4c22dab..c77d2a0 100644 --- a/src/components/GitlabProjectInfo.test.tsx +++ b/src/components/GitlabProjectInfo.test.tsx @@ -48,4 +48,32 @@ describe("GitlabProjectInfo", () => { render(); expect(screen.queryByRole("img")).not.toBeInTheDocument(); }); + + it("renders compact release, commit, and issue lines after the description", () => { + render(); + expect(screen.getByText("First")).toBeInTheDocument(); + expect(screen.getByText("v1.0")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "a1b2c3d" })).toHaveAttribute("href", "https://gitlab.com/c/a1b2c3d"); + expect(screen.getByText("fix: bug")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: /Broken thing/ })).toHaveAttribute("href", "https://gitlab.com/i/42"); + }); + + it("renders no section blocks when arrays are absent", () => { + const { container } = render(); + expect(container.querySelector(".gitlab-section")).toBeNull(); + }); + + it("overrides the title link when link is provided", () => { + render(); + expect(screen.getByRole("link", { name: "My Repo" })).toHaveAttribute("href", "https://example.com/app"); + }); + + it("defaults the title link to the project webUrl", () => { + render(); + expect(screen.getByRole("link", { name: "My Repo" })).toHaveAttribute("href", "https://gitlab.com/g/r"); + }); }); diff --git a/src/components/GitlabProjectInfo.tsx b/src/components/GitlabProjectInfo.tsx index f741faf..3bd05d2 100644 --- a/src/components/GitlabProjectInfo.tsx +++ b/src/components/GitlabProjectInfo.tsx @@ -1,25 +1,95 @@ import React from "react"; import { Fallback } from "./Fallback.js"; import { formatCount } from "./format.js"; -import type { ComponentPayload, ProjectInfoData } from "./types.js"; +import type { ComponentPayload, ProjectInfoData, ReleaseData, CommitData, IssueData } from "./types.js"; -export function GitlabProjectInfo({ data, error, showStats = true }: ComponentPayload & { showStats?: boolean }) { +type SectionLayout = "list" | "cards"; + +interface ProjectInfoProps extends ComponentPayload { + showStats?: boolean; + link?: string; + releasesLayout?: SectionLayout; + commitsLayout?: SectionLayout; + issuesLayout?: SectionLayout; +} + +function shortDate(iso: string): string { + return new Date(iso).toLocaleDateString(); +} + +function Releases({ items, layout }: { items: ReleaseData[]; layout: SectionLayout }) { + return ( +
+
Releases
+
    + {items.map((r) => ( +
  • + {r.tagName} + {r.name || r.tagName} + {layout === "cards" && ( + · {shortDate(r.releasedAt)} + )} +
  • + ))} +
+
+ ); +} + +function Commits({ items, layout }: { items: CommitData[]; layout: SectionLayout }) { + return ( +
+
Latest commits
+
    + {items.map((c) => ( +
  • + {c.shortId} + {c.title} + · {c.authorName} · {shortDate(c.createdAt)} +
  • + ))} +
+
+ ); +} + +function Issues({ items, layout }: { items: IssueData[]; layout: SectionLayout }) { + return ( +
+
Issues
+
    + {items.map((i) => ( +
  • + #{i.iid} {i.title} + {layout === "cards" && ( + · {i.state} · {i.authorName} + )} +
  • + ))} +
+
+ ); +} + +export function GitlabProjectInfo({ + data, + error, + showStats = true, + link, + releasesLayout = "list", + commitsLayout = "list", + issuesLayout = "list", +}: ProjectInfoProps) { if (error) return ; if (!data) return null; return (
{data.avatarUrl && ( - {data.name} + {data.name} )}
{data.descriptionHtml && ( @@ -28,6 +98,15 @@ export function GitlabProjectInfo({ data, error, showStats = true }: ComponentPa dangerouslySetInnerHTML={{ __html: data.descriptionHtml }} /> )} + {data.releases && data.releases.length > 0 && ( + + )} + {data.commits && data.commits.length > 0 && ( + + )} + {data.issues && data.issues.length > 0 && ( + + )} {data.topics.length > 0 && (
{data.topics.map((t) => ( diff --git a/src/components/types.ts b/src/components/types.ts index d2e1810..8d488a0 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -1,6 +1,7 @@ export type { ProjectInfoData, ReleaseData, + CommitData, IssueData, ReadmeData, FileData, From 72e7ec2047648c6243f5373942bb86584f29d34c Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 09:39:26 +0200 Subject: [PATCH 09/28] feat: export CommitData and cover cards layout Signed-off-by: Thomas Decaux --- src/components/GitlabProjectInfo.test.tsx | 8 ++++++++ src/components/index.ts | 1 + src/index.ts | 1 + 3 files changed, 10 insertions(+) diff --git a/src/components/GitlabProjectInfo.test.tsx b/src/components/GitlabProjectInfo.test.tsx index c77d2a0..16c87fc 100644 --- a/src/components/GitlabProjectInfo.test.tsx +++ b/src/components/GitlabProjectInfo.test.tsx @@ -76,4 +76,12 @@ describe("GitlabProjectInfo", () => { render(); expect(screen.getByRole("link", { name: "My Repo" })).toHaveAttribute("href", "https://gitlab.com/g/r"); }); + + it("shows richer metadata in cards layout", () => { + render(); + expect(screen.getByText(/opened/)).toBeInTheDocument(); + expect(screen.getByText(/Ada/)).toBeInTheDocument(); + }); }); diff --git a/src/components/index.ts b/src/components/index.ts index b9ad787..a97be37 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,6 +10,7 @@ export type { ProjectInfoData, ReleaseData, IssueData, + CommitData, ReadmeData, FileData, TopicData, diff --git a/src/index.ts b/src/index.ts index 43a0f20..b5d80e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ export type { ProjectInfoData, ReleaseData, IssueData, + CommitData, ReadmeData, FileData, TopicData, From 797b0925e1ddcef38876c1c1578a8bad5b5e5ae4 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 09:47:46 +0200 Subject: [PATCH 10/28] docs: document GitlabProjectInfo embedded sections Signed-off-by: Thomas Decaux --- README.md | 24 ++++++++++++++- .../site/docs/components/project-info.mdx | 30 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 097a6cb..e1c5d0d 100644 --- a/README.md +++ b/README.md @@ -89,17 +89,39 @@ namespace path (`project="group/subgroup/repo"`). ### `` -A card with name, description, topics, stars/forks, and last activity. +A card with name, description, topics, stars/forks, and last activity. It can +also embed compact **releases**, **commits**, and **issues** sections — each is +opt-in and only fetched when its count prop is a positive number. ```mdx + + + + ``` | Prop | Type | Default | Description | |---|---|---|---| | `project` | string \| number | — | **Required.** Project path or ID | | `showStats` | boolean | `true` | Show stars/forks/last-activity row | +| `link` | string | project's `web_url` | Override the card title's link target | +| `releases` | number | — | Embed the latest N releases. Absent or `≤ 0` — not fetched, not rendered | +| `commits` | number | — | Embed the latest N commits. Absent or `≤ 0` — not fetched, not rendered | +| `issues` | number | — | Embed the latest N issues. Absent or `≤ 0` — not fetched, not rendered | +| `releasesLayout` | `"list"` \| `"cards"` | `"list"` | Layout for the releases section. An invalid value fails the build | +| `commitsLayout` | `"list"` \| `"cards"` | `"list"` | Layout for the commits section. An invalid value fails the build | +| `issuesLayout` | `"list"` \| `"cards"` | `"list"` | Layout for the issues section. An invalid value fails the build | + +> Each section's `list` layout renders one compact line per item — release: +> tag + name; commit: linked short SHA + title + author + date; issue: +> linked `#iid` + title. `cards` renders a richer variant of the same data. ### `` diff --git a/examples/site/docs/components/project-info.mdx b/examples/site/docs/components/project-info.mdx index 957dda5..3f3d81e 100644 --- a/examples/site/docs/components/project-info.mdx +++ b/examples/site/docs/components/project-info.mdx @@ -8,12 +8,25 @@ sidebar_position: 1 Renders a card with a project's name, description, topics, star/fork counts, and last activity. Data is fetched at build time. +It can also embed compact **releases**, **commits**, and **issues** sections +directly in the card — each section is opt-in: it's only fetched (and only +rendered) when its count prop is a positive number. + ## Usage ```mdx + + + + ``` ## Props @@ -22,8 +35,23 @@ last activity. Data is fetched at build time. |---|---|---|---| | `project` | `string \| number` | — | **Required.** Project path (`group/sub/repo`) or numeric ID. | | `showStats` | `boolean` | `true` | Show the stars / forks / last-activity row. | +| `link` | `string` | project's `web_url` | Override the card title's link target. | +| `releases` | `number` | — | Embed the latest N releases. Absent or `≤ 0` ⇒ not fetched, not rendered. | +| `commits` | `number` | — | Embed the latest N commits. Absent or `≤ 0` ⇒ not fetched, not rendered. | +| `issues` | `number` | — | Embed the latest N issues. Absent or `≤ 0` ⇒ not fetched, not rendered. | +| `releasesLayout` | `"list" \| "cards"` | `"list"` | Layout for the embedded releases section. | +| `commitsLayout` | `"list" \| "cards"` | `"list"` | Layout for the embedded commits section. | +| `issuesLayout` | `"list" \| "cards"` | `"list"` | Layout for the embedded issues section. | ## Notes -- The link in the card points to the project's GitLab page (`web_url`). +- The link in the card points to the project's GitLab page (`web_url`) unless + overridden with `link`. +- Embedded sections are independent: any combination of `releases`, `commits`, + and `issues` may be set, in any order. +- `*Layout` props default to `"list"` — a compact single line per item: + release = tag + name; commit = linked short SHA + title + author + date; + issue = linked `#iid` + title. `"cards"` renders a richer variant of the + same data. An invalid layout value (anything other than `"list"` or + `"cards"`) fails the build. - On a failed fetch in non-strict mode, a fallback notice is rendered instead. From b2bbf3a4131e6a8d49445dc78c3fbc3c845ea59a Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 09:52:39 +0200 Subject: [PATCH 11/28] docs: single-line GitlabProjectInfo cards example Signed-off-by: Thomas Decaux --- README.md | 7 +------ examples/site/docs/components/project-info.mdx | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e1c5d0d..fc62335 100644 --- a/README.md +++ b/README.md @@ -99,12 +99,7 @@ opt-in and only fetched when its count prop is a positive number. - + ``` | Prop | Type | Default | Description | diff --git a/examples/site/docs/components/project-info.mdx b/examples/site/docs/components/project-info.mdx index 3f3d81e..1ba60b4 100644 --- a/examples/site/docs/components/project-info.mdx +++ b/examples/site/docs/components/project-info.mdx @@ -21,12 +21,7 @@ rendered) when its count prop is a positive number. - + ``` ## Props From 5e264c70b9f1ac1583dd5f4080419bf9f2ee3cf5 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 09:53:58 +0200 Subject: [PATCH 12/28] feat: add formatBytes helper Signed-off-by: Thomas Decaux --- src/components/formatBytes.test.ts | 15 +++++++++++++++ src/components/formatBytes.ts | 12 ++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/components/formatBytes.test.ts create mode 100644 src/components/formatBytes.ts diff --git a/src/components/formatBytes.test.ts b/src/components/formatBytes.test.ts new file mode 100644 index 0000000..6510f41 --- /dev/null +++ b/src/components/formatBytes.test.ts @@ -0,0 +1,15 @@ +import { describe, it, expect } from "vitest"; +import { formatBytes } from "./formatBytes.js"; + +describe("formatBytes", () => { + it("formats zero and bytes", () => { + expect(formatBytes(0)).toBe("0 B"); + expect(formatBytes(512)).toBe("512 B"); + }); + it("formats KB, MB, GB with one decimal", () => { + expect(formatBytes(1024)).toBe("1 KB"); + expect(formatBytes(1536)).toBe("1.5 KB"); + expect(formatBytes(4_404_019)).toBe("4.2 MB"); + expect(formatBytes(2_147_483_648)).toBe("2 GB"); + }); +}); diff --git a/src/components/formatBytes.ts b/src/components/formatBytes.ts new file mode 100644 index 0000000..e58150c --- /dev/null +++ b/src/components/formatBytes.ts @@ -0,0 +1,12 @@ +/** Humanize a byte count: 1536 -> "1.5 KB", 4.4e6 -> "4.2 MB". */ +export function formatBytes(n: number): string { + if (n < 1024) return `${n} B`; + const units = ["KB", "MB", "GB", "TB"]; + let value = n; + let unit = -1; + do { + value /= 1024; + unit += 1; + } while (value >= 1024 && unit < units.length - 1); + return `${parseFloat(value.toFixed(1))} ${units[unit]}`; +} From dee53773c21f5973bbb46feec4f13951ff8611b3 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 09:58:21 +0200 Subject: [PATCH 13/28] feat: add statistics option and getContributorsCount to client Signed-off-by: Thomas Decaux --- src/gitlab/client.test.ts | 31 +++++++++++++++++++++++++++++++ src/gitlab/client.ts | 14 ++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/gitlab/client.test.ts b/src/gitlab/client.test.ts index 607bc40..9c243e5 100644 --- a/src/gitlab/client.test.ts +++ b/src/gitlab/client.test.ts @@ -10,6 +10,7 @@ const topicsAllMock = vi.fn(); const projectLabelsAllMock = vi.fn(); const groupLabelsAllMock = vi.fn(); const groupShowMock = vi.fn(); +const contributorsAllMock = vi.fn(); vi.mock("@gitbeaker/rest", () => ({ // Vitest 4 invokes the mock implementation as a real constructor under @@ -26,6 +27,7 @@ vi.mock("@gitbeaker/rest", () => ({ ProjectLabels: { all: projectLabelsAllMock }, GroupLabels: { all: groupLabelsAllMock }, Groups: { show: groupShowMock }, + Repositories: { allContributors: contributorsAllMock }, }; }), })); @@ -46,6 +48,7 @@ beforeEach(() => { projectLabelsAllMock.mockReset(); groupLabelsAllMock.mockReset(); groupShowMock.mockReset(); + contributorsAllMock.mockReset(); }); afterEach(() => { vi.unstubAllGlobals(); @@ -186,4 +189,32 @@ describe("GitLabClient", () => { expect(data).toEqual({ id: 9, web_url: "https://x/groups/my-group" }); expect(groupShowMock).toHaveBeenCalledWith("my-group"); }); + + it("getProject forwards the statistics option", async () => { + showMock.mockResolvedValue({ id: 1 }); + const client = new GitLabClient({ host: "https://gitlab.com" }); + await client.getProject("g/r", { statistics: true }); + expect(showMock).toHaveBeenCalledWith("g/r", { statistics: true }); + }); + + it("getProject omits options by default", async () => { + showMock.mockResolvedValue({ id: 1 }); + const client = new GitLabClient({ host: "https://gitlab.com" }); + await client.getProject("g/r"); + expect(showMock).toHaveBeenCalledWith("g/r"); + }); + + it("getContributorsCount returns the pagination total", async () => { + contributorsAllMock.mockResolvedValue({ data: [{}], paginationInfo: { total: 8 } }); + const client = new GitLabClient({ host: "https://gitlab.com" }); + const count = await client.getContributorsCount("g/r"); + expect(contributorsAllMock).toHaveBeenCalledWith("g/r", { showExpanded: true, perPage: 1, maxPages: 1 }); + expect(count).toBe(8); + }); + + it("getContributorsCount returns undefined when total is absent", async () => { + contributorsAllMock.mockResolvedValue({ data: [], paginationInfo: {} }); + const client = new GitLabClient({ host: "https://gitlab.com" }); + expect(await client.getContributorsCount("g/r")).toBeUndefined(); + }); }); diff --git a/src/gitlab/client.ts b/src/gitlab/client.ts index 35537d6..fe9faac 100644 --- a/src/gitlab/client.ts +++ b/src/gitlab/client.ts @@ -36,8 +36,18 @@ export class GitLabClient { : new Gitlab({ host: config.host }); } - async getProject(project: ProjectRef): Promise { - return this.api.Projects.show(project); + async getProject(project: ProjectRef, opts?: { statistics?: boolean }): Promise { + return opts ? this.api.Projects.show(project, opts) : this.api.Projects.show(project); + } + + async getContributorsCount(project: ProjectRef): Promise { + const res: any = await this.api.Repositories.allContributors(project, { + showExpanded: true, + perPage: 1, + maxPages: 1, + } as any); + const total = res?.paginationInfo?.total; + return typeof total === "number" ? total : undefined; } async getReleases(project: ProjectRef, limit: number): Promise { From 213a06e08d3ff3c61e20a07978b46f06b0b61f49 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 10:03:31 +0200 Subject: [PATCH 14/28] fix: guard getContributorsCount against NaN X-Total Signed-off-by: Thomas Decaux --- src/gitlab/client.test.ts | 6 ++++++ src/gitlab/client.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/gitlab/client.test.ts b/src/gitlab/client.test.ts index 9c243e5..0146bbb 100644 --- a/src/gitlab/client.test.ts +++ b/src/gitlab/client.test.ts @@ -217,4 +217,10 @@ describe("GitLabClient", () => { const client = new GitLabClient({ host: "https://gitlab.com" }); expect(await client.getContributorsCount("g/r")).toBeUndefined(); }); + + it("getContributorsCount returns undefined when total is NaN", async () => { + contributorsAllMock.mockResolvedValue({ data: [], paginationInfo: { total: NaN } }); + const client = new GitLabClient({ host: "https://gitlab.com" }); + expect(await client.getContributorsCount("g/r")).toBeUndefined(); + }); }); diff --git a/src/gitlab/client.ts b/src/gitlab/client.ts index fe9faac..9be03ef 100644 --- a/src/gitlab/client.ts +++ b/src/gitlab/client.ts @@ -47,7 +47,7 @@ export class GitLabClient { maxPages: 1, } as any); const total = res?.paginationInfo?.total; - return typeof total === "number" ? total : undefined; + return Number.isFinite(total) ? total : undefined; } async getReleases(project: ProjectRef, limit: number): Promise { From 001ca6ab4dfb09ea589ee2210b8d1693653abe91 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 10:07:16 +0200 Subject: [PATCH 15/28] feat: map commit/contributor/issue/size stats in fetchProjectInfo Signed-off-by: Thomas Decaux --- src/gitlab/fetchers.test.ts | 54 ++++++++++++++++++++++++++++++++++++- src/gitlab/fetchers.ts | 12 ++++++++- src/gitlab/types.ts | 4 +++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/gitlab/fetchers.test.ts b/src/gitlab/fetchers.test.ts index c5ce477..0db2ce3 100644 --- a/src/gitlab/fetchers.test.ts +++ b/src/gitlab/fetchers.test.ts @@ -33,7 +33,59 @@ describe("fetchProjectInfo", () => { expect(data.descriptionHtml).toContain("🚀"); expect(data.avatarUrl).toBeNull(); expect(c.assets.localize).not.toHaveBeenCalled(); - expect(client.getProject).toHaveBeenCalledWith("g/r"); + expect(client.getProject).toHaveBeenCalledWith("g/r", { statistics: true }); + }); + + it("maps statistics, open issues, and contributors count", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + issues_enabled: true, open_issues_count: 12, + statistics: { commit_count: 1200, repository_size: 4404019 }, + })), + getContributorsCount: vi.fn(async () => 8), + }; + const c = ctx(client); + const data = await fetchProjectInfo(c, { project: "g/r" }); + expect(client.getProject).toHaveBeenCalledWith("g/r", { statistics: true }); + expect(data.commitCount).toBe(1200); + expect(data.repositorySize).toBe(4404019); + expect(data.openIssuesCount).toBe(12); + expect(data.contributorsCount).toBe(8); + }); + + it("omits statistics-derived stats when statistics is absent", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + issues_enabled: false, + })), + getContributorsCount: vi.fn(async () => undefined), + }; + const c = ctx(client); + const data = await fetchProjectInfo(c, { project: "g/r" }); + expect(data.commitCount).toBeUndefined(); + expect(data.repositorySize).toBeUndefined(); + expect(data.openIssuesCount).toBeUndefined(); + expect(data.contributorsCount).toBeUndefined(); + }); + + it("never throws when the contributors fetch fails, even in strict mode", async () => { + const client = { + getProject: vi.fn(async () => ({ + id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", + star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + issues_enabled: true, open_issues_count: 5, statistics: { commit_count: 1, repository_size: 1 }, + })), + getContributorsCount: vi.fn(async () => { throw new Error("no perms"); }), + }; + const c = ctx(client); + c.options.strict = true; + const data = await fetchProjectInfo(c, { project: "g/r" }); + expect(data.contributorsCount).toBeUndefined(); + expect(data.commitCount).toBe(1); }); it("localizes the avatar when the project has one", async () => { diff --git a/src/gitlab/fetchers.ts b/src/gitlab/fetchers.ts index fa5bb75..f7289f0 100644 --- a/src/gitlab/fetchers.ts +++ b/src/gitlab/fetchers.ts @@ -104,8 +104,14 @@ export async function fetchProjectInfo(ctx: GitLabContext, attrs: Attrs): Promis } return memo(ctx, `projectInfo:${project}:r${rN}:c${cN}:i${iN}`, async () => { - const p = await ctx.client.getProject(attrs.project as string | number); + const p = await ctx.client.getProject(attrs.project as string | number, { statistics: true }); const avatarUrl = p.avatar_url ? await ctx.assets.localize(p.avatar_url, "", project) : null; + let contributorsCount: number | undefined; + try { + contributorsCount = await ctx.client.getContributorsCount(attrs.project as string | number); + } catch { + contributorsCount = undefined; + } const [releases, commits, issues] = await Promise.all([ section(rN, () => fetchReleases(ctx, { project, limit: rN })), section(cN, () => fetchCommits(ctx, { project, limit: cN })), @@ -123,6 +129,10 @@ export async function fetchProjectInfo(ctx: GitLabContext, attrs: Attrs): Promis lastActivityAt: p.last_activity_at, avatarUrl, }; + if (typeof p.statistics?.commit_count === "number") base.commitCount = p.statistics.commit_count; + if (typeof p.statistics?.repository_size === "number") base.repositorySize = p.statistics.repository_size; + if (p.issues_enabled && typeof p.open_issues_count === "number") base.openIssuesCount = p.open_issues_count; + if (typeof contributorsCount === "number") base.contributorsCount = contributorsCount; if (releases) base.releases = releases; if (commits) base.commits = commits; if (issues) base.issues = issues; diff --git a/src/gitlab/types.ts b/src/gitlab/types.ts index c122b83..234131d 100644 --- a/src/gitlab/types.ts +++ b/src/gitlab/types.ts @@ -17,6 +17,10 @@ export interface ProjectInfoData { releases?: ReleaseData[]; commits?: CommitData[]; issues?: IssueData[]; + openIssuesCount?: number; + commitCount?: number; + repositorySize?: number; + contributorsCount?: number; } export interface ReleaseAsset { From 1504a14f10e85b14c10b3eab3ef8f51f67faa701 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 10:16:19 +0200 Subject: [PATCH 16/28] refactor: surface programmer errors in contributors fetch; fix test fakes Signed-off-by: Thomas Decaux --- src/gitlab/fetchers.test.ts | 7 +++++++ src/gitlab/fetchers.ts | 9 +++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/gitlab/fetchers.test.ts b/src/gitlab/fetchers.test.ts index 0db2ce3..af7ffb7 100644 --- a/src/gitlab/fetchers.test.ts +++ b/src/gitlab/fetchers.test.ts @@ -25,6 +25,7 @@ describe("fetchProjectInfo", () => { id: 7, path_with_namespace: "g/r", name: "r", description: "ship **it** :rocket:", web_url: "https://gitlab.com/g/r", star_count: 3, forks_count: 1, topics: ["x"], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, })), + getContributorsCount: vi.fn(async () => undefined), }; const c = ctx(client); const data = await fetchProjectInfo(c, { project: "g/r" }); @@ -95,6 +96,7 @@ describe("fetchProjectInfo", () => { star_count: 3, forks_count: 1, topics: ["x"], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: "https://gitlab.com/uploads/avatar.png", })), + getContributorsCount: vi.fn(async () => undefined), }; const c = ctx(client); const data = await fetchProjectInfo(c, { project: "g/r" }); @@ -115,6 +117,7 @@ describe("fetchProjectInfo", () => { { short_id: "a1", title: "t", web_url: "u", author_name: "Ada", created_at: "2026-01-02T00:00:00Z" }, ]), getIssues: vi.fn(async () => []), + getContributorsCount: vi.fn(async () => undefined), }; const c = ctx(client); const data = await fetchProjectInfo(c, { project: "g/r", releases: 2, commits: 3 }); @@ -135,6 +138,7 @@ describe("fetchProjectInfo", () => { getReleases: vi.fn(async () => []), getCommits: vi.fn(async () => []), getIssues: vi.fn(async () => []), + getContributorsCount: vi.fn(async () => undefined), }; const c = ctx(client); const data = await fetchProjectInfo(c, { project: "g/r", commits: 0 }); @@ -151,6 +155,7 @@ describe("fetchProjectInfo", () => { star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, })), getReleases: vi.fn(async () => { throw new Error("boom"); }), + getContributorsCount: vi.fn(async () => undefined), }; const c = ctx(client); c.options.strict = false; @@ -166,6 +171,7 @@ describe("fetchProjectInfo", () => { star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, })), getReleases: vi.fn(async () => { throw new Error("boom"); }), + getContributorsCount: vi.fn(async () => undefined), }; const c = ctx(client); c.options.strict = true; @@ -191,6 +197,7 @@ describe("fetchProjectInfo", () => { ), getCommits: vi.fn(async () => []), getIssues: vi.fn(async () => []), + getContributorsCount: vi.fn(async () => undefined), }; const c = ctx(client); const first = await fetchProjectInfo(c, { project: "g/r", releases: 1 }); diff --git a/src/gitlab/fetchers.ts b/src/gitlab/fetchers.ts index f7289f0..6e4ae49 100644 --- a/src/gitlab/fetchers.ts +++ b/src/gitlab/fetchers.ts @@ -106,12 +106,9 @@ export async function fetchProjectInfo(ctx: GitLabContext, attrs: Attrs): Promis return memo(ctx, `projectInfo:${project}:r${rN}:c${cN}:i${iN}`, async () => { const p = await ctx.client.getProject(attrs.project as string | number, { statistics: true }); const avatarUrl = p.avatar_url ? await ctx.assets.localize(p.avatar_url, "", project) : null; - let contributorsCount: number | undefined; - try { - contributorsCount = await ctx.client.getContributorsCount(attrs.project as string | number); - } catch { - contributorsCount = undefined; - } + const contributorsCount = await ctx.client + .getContributorsCount(attrs.project as string | number) + .catch(() => undefined); const [releases, commits, issues] = await Promise.all([ section(rN, () => fetchReleases(ctx, { project, limit: rN })), section(cN, () => fetchCommits(ctx, { project, limit: cN })), From 491b93e5bc684f392643dcf13ac1a7ab07759445 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 12:51:55 +0200 Subject: [PATCH 17/28] feat: render extended stat pills in GitlabProjectInfo Signed-off-by: Thomas Decaux --- src/components/GitlabProjectInfo.test.tsx | 22 ++++++++++++++++++++++ src/components/GitlabProjectInfo.tsx | 13 +++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/components/GitlabProjectInfo.test.tsx b/src/components/GitlabProjectInfo.test.tsx index 16c87fc..babe7c6 100644 --- a/src/components/GitlabProjectInfo.test.tsx +++ b/src/components/GitlabProjectInfo.test.tsx @@ -84,4 +84,26 @@ describe("GitlabProjectInfo", () => { expect(screen.getByText(/opened/)).toBeInTheDocument(); expect(screen.getByText(/Ada/)).toBeInTheDocument(); }); + + it("appends stat pills when their data is present", () => { + render(); + expect(screen.getByText(/1.2k commits/)).toBeInTheDocument(); + expect(screen.getByText(/8 contributors/)).toBeInTheDocument(); + expect(screen.getByText(/12 issues/)).toBeInTheDocument(); + expect(screen.getByText(/4.2 MB/)).toBeInTheDocument(); + }); + + it("omits stat pills whose data is absent", () => { + render(); + expect(screen.queryByText(/commits/)).not.toBeInTheDocument(); + expect(screen.queryByText(/contributors/)).not.toBeInTheDocument(); + }); + + it("hides all stats including new pills when showStats is false", () => { + render(); + expect(screen.queryByText(/commits/)).not.toBeInTheDocument(); + expect(screen.queryByText(/★/)).not.toBeInTheDocument(); + }); }); diff --git a/src/components/GitlabProjectInfo.tsx b/src/components/GitlabProjectInfo.tsx index 3bd05d2..6e5be37 100644 --- a/src/components/GitlabProjectInfo.tsx +++ b/src/components/GitlabProjectInfo.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Fallback } from "./Fallback.js"; import { formatCount } from "./format.js"; +import { formatBytes } from "./formatBytes.js"; import type { ComponentPayload, ProjectInfoData, ReleaseData, CommitData, IssueData } from "./types.js"; type SectionLayout = "list" | "cards"; @@ -118,6 +119,18 @@ export function GitlabProjectInfo({
★ {formatCount(data.starCount)} ⑂ {formatCount(data.forksCount)} + {typeof data.commitCount === "number" && ( + ⎇ {formatCount(data.commitCount)} commits + )} + {typeof data.contributorsCount === "number" && ( + 👥 {formatCount(data.contributorsCount)} contributors + )} + {typeof data.openIssuesCount === "number" && ( + ⊙ {formatCount(data.openIssuesCount)} issues + )} + {typeof data.repositorySize === "number" && ( + ▤ {formatBytes(data.repositorySize)} + )} updated {new Date(data.lastActivityAt).toLocaleDateString()}
)} From e7951c0b479a511a5ee7c9a40bb24dd5e5776d3c Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 12:56:30 +0200 Subject: [PATCH 18/28] docs: document GitlabProjectInfo extended stats Signed-off-by: Thomas Decaux --- README.md | 9 +++++++++ examples/site/docs/components/project-info.mdx | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/README.md b/README.md index fc62335..3ed77c0 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,15 @@ opt-in and only fetched when its count prop is a positive number. > Each section's `list` layout renders one compact line per item — release: > tag + name; commit: linked short SHA + title + author + date; issue: > linked `#iid` + title. `cards` renders a richer variant of the same data. +> +> The `showStats` row can also show extra pills — total commit count, +> contributor count, open issue count, repository size — automatically, +> whenever that data is available; there's no attribute to request them. Set +> `showStats={false}` to hide the whole row (pills included). Commit count and +> repository size come from the project's `statistics`, which GitLab only +> returns to tokens with **Reporter role or higher**; on anonymous/public +> builds those two pills are simply omitted. Contributor count also depends +> on the API returning a total count header, and is omitted if it isn't. ### `` diff --git a/examples/site/docs/components/project-info.mdx b/examples/site/docs/components/project-info.mdx index 1ba60b4..da2f65f 100644 --- a/examples/site/docs/components/project-info.mdx +++ b/examples/site/docs/components/project-info.mdx @@ -50,3 +50,18 @@ rendered) when its count prop is a positive number. same data. An invalid layout value (anything other than `"list"` or `"cards"`) fails the build. - On a failed fetch in non-strict mode, a fallback notice is rendered instead. +- The stats row (`showStats`) can show up to four extra pills — total commit + count, contributor count, open issue count, and repository size — but only + when that data is available. There's no prop to request them; they just + appear when GitLab returns the underlying data, and disappear along with + the rest of the row when `showStats={false}`. + - Commit count and repository size come from the project's `statistics`, + which GitLab only includes in the API response for tokens with + **Reporter role or higher**. On anonymous or public builds, those two + pills are omitted. + - Contributor count relies on the contributors API returning a total-count + header; if it's unavailable, that pill is omitted too. + - Open issue count has no such requirement (as long as the project has + issues enabled). + - There's no lines-of-code pill — GitLab has no API for it, so it's + intentionally not offered. From 39f38d93b0de38c826716a31d1f5e7388a47b289 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 13:04:21 +0200 Subject: [PATCH 19/28] style: theme the embedded GitlabProjectInfo sections and cards layout Signed-off-by: Thomas Decaux --- theme.css | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/theme.css b/theme.css index 47fd175..06ba977 100644 --- a/theme.css +++ b/theme.css @@ -236,6 +236,49 @@ margin-top: 0.75rem; } +/* Embedded sections inside the project card (releases / commits / issues) */ +.gitlab-section { + margin-top: 0.85rem; +} +.gitlab-section-title { + font-size: 0.72em; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 0.35rem; +} +.gitlab-section-list { + list-style: none; + margin: 0; + padding: 0; +} +.gitlab-section-item { + padding: 0.2rem 0; + font-size: 0.9em; + line-height: 1.45; +} +.gitlab-section-name { + font-weight: 500; +} +.gitlab-commit-sha { + font-family: var(--ifm-font-family-monospace); + font-size: 0.85em; + color: var(--ifm-color-primary); +} + +/* Cards layout: each item becomes a bordered chip-card instead of a plain line */ +.gitlab-section-list[data-layout="cards"] { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 0.5rem; +} +.gitlab-section-list[data-layout="cards"] .gitlab-section-item { + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 8px; + padding: 0.5rem 0.75rem; + background: var(--ifm-background-surface-color); +} + /* README / markdown file embed */ .gitlab-readme img { max-width: 100%; @@ -276,6 +319,9 @@ [data-theme='dark'] .gitlab-release { box-shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 2px 10px rgb(0 0 0 / 0.25); } +[data-theme='dark'] .gitlab-section-list[data-layout="cards"] .gitlab-section-item { + box-shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 2px 10px rgb(0 0 0 / 0.25); +} /* Generated [[_TOC_]] table of contents */ .gitlab-md-toc ul { From 3b683ca30999d3509f916e36aa540f4c3ebe489d Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 13:41:35 +0200 Subject: [PATCH 20/28] feat: relative dates for commits/issues; right-aligned issue date Signed-off-by: Thomas Decaux --- README.md | 6 +++-- .../site/docs/components/project-info.mdx | 9 ++++--- src/components/GitlabProjectInfo.test.tsx | 9 +++++++ src/components/GitlabProjectInfo.tsx | 4 ++- src/components/timeAgo.test.ts | 25 +++++++++++++++++++ src/components/timeAgo.ts | 19 ++++++++++++++ theme.css | 15 ++++++++++- 7 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 src/components/timeAgo.test.ts create mode 100644 src/components/timeAgo.ts diff --git a/README.md b/README.md index 3ed77c0..efb1985 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,10 @@ opt-in and only fetched when its count prop is a positive number. | `issuesLayout` | `"list"` \| `"cards"` | `"list"` | Layout for the issues section. An invalid value fails the build | > Each section's `list` layout renders one compact line per item — release: -> tag + name; commit: linked short SHA + title + author + date; issue: -> linked `#iid` + title. `cards` renders a richer variant of the same data. +> tag + name; commit: linked short SHA + title + author + relative date; +> issue: linked `#iid` + title with a relative date pinned to the right. +> Commit and issue dates use a relative `"x ago"` format. `cards` renders a +> richer variant of the same data. > > The `showStats` row can also show extra pills — total commit count, > contributor count, open issue count, repository size — automatically, diff --git a/examples/site/docs/components/project-info.mdx b/examples/site/docs/components/project-info.mdx index da2f65f..4dc6dab 100644 --- a/examples/site/docs/components/project-info.mdx +++ b/examples/site/docs/components/project-info.mdx @@ -45,10 +45,11 @@ rendered) when its count prop is a positive number. - Embedded sections are independent: any combination of `releases`, `commits`, and `issues` may be set, in any order. - `*Layout` props default to `"list"` — a compact single line per item: - release = tag + name; commit = linked short SHA + title + author + date; - issue = linked `#iid` + title. `"cards"` renders a richer variant of the - same data. An invalid layout value (anything other than `"list"` or - `"cards"`) fails the build. + release = tag + name; commit = linked short SHA + title + author + relative + date; issue = linked `#iid` + title with a relative date on the right. + Commit and issue dates use a relative `"x ago"` format. `"cards"` renders a + richer variant of the same data. An invalid layout value (anything other + than `"list"` or `"cards"`) fails the build. - On a failed fetch in non-strict mode, a fallback notice is rendered instead. - The stats row (`showStats`) can show up to four extra pills — total commit count, contributor count, open issue count, and repository size — but only diff --git a/src/components/GitlabProjectInfo.test.tsx b/src/components/GitlabProjectInfo.test.tsx index babe7c6..6eca290 100644 --- a/src/components/GitlabProjectInfo.test.tsx +++ b/src/components/GitlabProjectInfo.test.tsx @@ -85,6 +85,15 @@ describe("GitlabProjectInfo", () => { expect(screen.getByText(/Ada/)).toBeInTheDocument(); }); + it("renders a relative 'ago' date on each issue", () => { + const { container } = render(); + const date = container.querySelector(".gitlab-section-issues .gitlab-section-date"); + expect(date).not.toBeNull(); + expect(date?.textContent).toMatch(/ago/); + }); + it("appends stat pills when their data is present", () => { render( {c.shortId} {c.title} - · {c.authorName} · {shortDate(c.createdAt)} + · {c.authorName} · {timeAgo(c.createdAt)} ))} @@ -65,6 +66,7 @@ function Issues({ items, layout }: { items: IssueData[]; layout: SectionLayout } {layout === "cards" && ( · {i.state} · {i.authorName} )} + {timeAgo(i.createdAt)} ))} diff --git a/src/components/timeAgo.test.ts b/src/components/timeAgo.test.ts new file mode 100644 index 0000000..084b013 --- /dev/null +++ b/src/components/timeAgo.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from "vitest"; +import { timeAgo } from "./timeAgo.js"; + +const now = Date.parse("2026-07-08T00:00:00Z"); + +describe("timeAgo", () => { + it("returns 'just now' for the current instant and the future", () => { + expect(timeAgo("2026-07-08T00:00:00Z", now)).toBe("just now"); + expect(timeAgo("2026-07-09T00:00:00Z", now)).toBe("just now"); + }); + + it("formats minutes and hours", () => { + expect(timeAgo("2026-07-07T23:59:00Z", now)).toBe("1 minute ago"); + expect(timeAgo("2026-07-07T23:00:00Z", now)).toBe("1 hour ago"); + expect(timeAgo("2026-07-07T21:00:00Z", now)).toBe("3 hours ago"); + }); + + it("formats days, weeks, months, and years with pluralization", () => { + expect(timeAgo("2026-07-07T00:00:00Z", now)).toBe("1 day ago"); + expect(timeAgo("2026-07-01T00:00:00Z", now)).toBe("1 week ago"); + expect(timeAgo("2026-05-08T00:00:00Z", now)).toBe("2 months ago"); + expect(timeAgo("2025-07-08T00:00:00Z", now)).toBe("1 year ago"); + expect(timeAgo("2024-07-08T00:00:00Z", now)).toBe("2 years ago"); + }); +}); diff --git a/src/components/timeAgo.ts b/src/components/timeAgo.ts new file mode 100644 index 0000000..b0b29f6 --- /dev/null +++ b/src/components/timeAgo.ts @@ -0,0 +1,19 @@ +/** Humanize a past timestamp relative to `now`: "3 hours ago", "2 months ago". */ +export function timeAgo(iso: string, now: number = Date.now()): string { + const secs = Math.max(0, Math.round((now - new Date(iso).getTime()) / 1000)); + const units: [number, string][] = [ + [31536000, "year"], + [2592000, "month"], + [604800, "week"], + [86400, "day"], + [3600, "hour"], + [60, "minute"], + ]; + for (const [size, label] of units) { + if (secs >= size) { + const n = Math.floor(secs / size); + return `${n} ${label}${n === 1 ? "" : "s"} ago`; + } + } + return "just now"; +} diff --git a/theme.css b/theme.css index 06ba977..faaad6d 100644 --- a/theme.css +++ b/theme.css @@ -44,7 +44,9 @@ .gitlab-stats { display: flex; gap: 1rem; - margin-top: 0.5rem; + margin-top: 1rem; + border-top: 1px solid lightgray; + padding-top: 0.5rem; } /* Badges: topics, tags, labels, release assets */ @@ -265,6 +267,17 @@ font-size: 0.85em; color: var(--ifm-color-primary); } +/* Issue rows pin their "x ago" date to the right edge. */ +.gitlab-section-issues .gitlab-section-item { + display: flex; + align-items: baseline; + gap: 0.35rem; +} +.gitlab-section-date { + margin-left: auto; + white-space: nowrap; + font-size: 0.85em; +} /* Cards layout: each item becomes a bordered chip-card instead of a plain line */ .gitlab-section-list[data-layout="cards"] { From cbceb9e5a42c4bcfd61396ed44e276b141164cc9 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 13:47:26 +0200 Subject: [PATCH 21/28] feat: right-align relative dates for releases and commits too Signed-off-by: Thomas Decaux --- README.md | 8 ++++---- examples/site/docs/components/project-info.mdx | 9 ++++----- src/components/GitlabProjectInfo.test.tsx | 11 +++++++---- src/components/GitlabProjectInfo.tsx | 11 +++-------- theme.css | 4 +++- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index efb1985..8b11889 100644 --- a/README.md +++ b/README.md @@ -115,10 +115,10 @@ opt-in and only fetched when its count prop is a positive number. | `issuesLayout` | `"list"` \| `"cards"` | `"list"` | Layout for the issues section. An invalid value fails the build | > Each section's `list` layout renders one compact line per item — release: -> tag + name; commit: linked short SHA + title + author + relative date; -> issue: linked `#iid` + title with a relative date pinned to the right. -> Commit and issue dates use a relative `"x ago"` format. `cards` renders a -> richer variant of the same data. +> tag + name; commit: linked short SHA + title + author; issue: linked `#iid` +> +> - title. Every item shows a relative `"x ago"` date pinned to the right. +> `cards` renders a richer variant of the same data. > > The `showStats` row can also show extra pills — total commit count, > contributor count, open issue count, repository size — automatically, diff --git a/examples/site/docs/components/project-info.mdx b/examples/site/docs/components/project-info.mdx index 4dc6dab..1553f19 100644 --- a/examples/site/docs/components/project-info.mdx +++ b/examples/site/docs/components/project-info.mdx @@ -45,11 +45,10 @@ rendered) when its count prop is a positive number. - Embedded sections are independent: any combination of `releases`, `commits`, and `issues` may be set, in any order. - `*Layout` props default to `"list"` — a compact single line per item: - release = tag + name; commit = linked short SHA + title + author + relative - date; issue = linked `#iid` + title with a relative date on the right. - Commit and issue dates use a relative `"x ago"` format. `"cards"` renders a - richer variant of the same data. An invalid layout value (anything other - than `"list"` or `"cards"`) fails the build. + release = tag + name; commit = linked short SHA + title + author; issue = + linked `#iid` + title. Every item shows a relative `"x ago"` date pinned to + the right. `"cards"` renders a richer variant of the same data. An invalid + layout value (anything other than `"list"` or `"cards"`) fails the build. - On a failed fetch in non-strict mode, a fallback notice is rendered instead. - The stats row (`showStats`) can show up to four extra pills — total commit count, contributor count, open issue count, and repository size — but only diff --git a/src/components/GitlabProjectInfo.test.tsx b/src/components/GitlabProjectInfo.test.tsx index 6eca290..68ae80a 100644 --- a/src/components/GitlabProjectInfo.test.tsx +++ b/src/components/GitlabProjectInfo.test.tsx @@ -85,13 +85,16 @@ describe("GitlabProjectInfo", () => { expect(screen.getByText(/Ada/)).toBeInTheDocument(); }); - it("renders a relative 'ago' date on each issue", () => { + it("renders a relative 'ago' date on each release, commit, and issue", () => { const { container } = render(); - const date = container.querySelector(".gitlab-section-issues .gitlab-section-date"); - expect(date).not.toBeNull(); - expect(date?.textContent).toMatch(/ago/); + for (const section of ["releases", "commits", "issues"]) { + const date = container.querySelector(`.gitlab-section-${section} .gitlab-section-date`); + expect(date?.textContent).toMatch(/ago/); + } }); it("appends stat pills when their data is present", () => { diff --git a/src/components/GitlabProjectInfo.tsx b/src/components/GitlabProjectInfo.tsx index 29e8df6..625af4f 100644 --- a/src/components/GitlabProjectInfo.tsx +++ b/src/components/GitlabProjectInfo.tsx @@ -15,10 +15,6 @@ interface ProjectInfoProps extends ComponentPayload { issuesLayout?: SectionLayout; } -function shortDate(iso: string): string { - return new Date(iso).toLocaleDateString(); -} - function Releases({ items, layout }: { items: ReleaseData[]; layout: SectionLayout }) { return (
@@ -28,9 +24,7 @@ function Releases({ items, layout }: { items: ReleaseData[]; layout: SectionLayo
  • {r.tagName} {r.name || r.tagName} - {layout === "cards" && ( - · {shortDate(r.releasedAt)} - )} + {timeAgo(r.releasedAt)}
  • ))} @@ -47,7 +41,8 @@ function Commits({ items, layout }: { items: CommitData[]; layout: SectionLayout
  • {c.shortId} {c.title} - · {c.authorName} · {timeAgo(c.createdAt)} + · {c.authorName} + {timeAgo(c.createdAt)}
  • ))} diff --git a/theme.css b/theme.css index faaad6d..0999880 100644 --- a/theme.css +++ b/theme.css @@ -267,7 +267,9 @@ font-size: 0.85em; color: var(--ifm-color-primary); } -/* Issue rows pin their "x ago" date to the right edge. */ +/* Release / commit / issue rows pin their "x ago" date to the right edge. */ +.gitlab-section-releases .gitlab-section-item, +.gitlab-section-commits .gitlab-section-item, .gitlab-section-issues .gitlab-section-item { display: flex; align-items: baseline; From 2f62ff8b388fab82ec90cce36f3689f8fb21470b Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 13:53:47 +0200 Subject: [PATCH 22/28] feat: show project path in green below the description Signed-off-by: Thomas Decaux --- src/components/GitlabProjectInfo.test.tsx | 7 +++++++ src/components/GitlabProjectInfo.tsx | 1 + theme.css | 8 ++++++++ 3 files changed, 16 insertions(+) diff --git a/src/components/GitlabProjectInfo.test.tsx b/src/components/GitlabProjectInfo.test.tsx index 68ae80a..a3b9186 100644 --- a/src/components/GitlabProjectInfo.test.tsx +++ b/src/components/GitlabProjectInfo.test.tsx @@ -62,6 +62,13 @@ describe("GitlabProjectInfo", () => { expect(screen.getByRole("link", { name: /Broken thing/ })).toHaveAttribute("href", "https://gitlab.com/i/42"); }); + it("renders the project path below the description", () => { + const { container } = render(); + const path = container.querySelector(".gitlab-path"); + expect(path).not.toBeNull(); + expect(path?.textContent).toBe("g/r"); + }); + it("renders no section blocks when arrays are absent", () => { const { container } = render(); expect(container.querySelector(".gitlab-section")).toBeNull(); diff --git a/src/components/GitlabProjectInfo.tsx b/src/components/GitlabProjectInfo.tsx index 625af4f..38e841f 100644 --- a/src/components/GitlabProjectInfo.tsx +++ b/src/components/GitlabProjectInfo.tsx @@ -96,6 +96,7 @@ export function GitlabProjectInfo({ dangerouslySetInnerHTML={{ __html: data.descriptionHtml }} /> )} +
    {data.path}
    {data.releases && data.releases.length > 0 && ( )} diff --git a/theme.css b/theme.css index 0999880..594c3b9 100644 --- a/theme.css +++ b/theme.css @@ -238,6 +238,14 @@ margin-top: 0.75rem; } +/* Project path (group/name), shown just below the description */ +.gitlab-path { + margin-top: 0.15rem; + color: var(--ifm-color-success); + font-family: var(--ifm-font-family-monospace); + font-size: 0.85em; +} + /* Embedded sections inside the project card (releases / commits / issues) */ .gitlab-section { margin-top: 0.85rem; From 629c077319ae39ab007ac661d87cca23695f6d05 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 14:11:54 +0200 Subject: [PATCH 23/28] feat: add showLinks prop to toggle release/commit/issue links Signed-off-by: Thomas Decaux --- README.md | 1 + .../site/docs/components/project-info.mdx | 1 + src/components/GitlabProjectInfo.test.tsx | 25 ++++++++++ src/components/GitlabProjectInfo.tsx | 50 +++++++++++++------ src/gitlab/fetchers.test.ts | 4 +- src/gitlab/fetchers.ts | 1 + src/gitlab/types.ts | 2 + 7 files changed, 67 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 8b11889..87694a9 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ opt-in and only fetched when its count prop is a positive number. |---|---|---|---| | `project` | string \| number | — | **Required.** Project path or ID | | `showStats` | boolean | `true` | Show stars/forks/last-activity row | +| `showLinks` | boolean | `true` | Link the release / commit / issue items. Set `false` to render them as plain text (the card title stays a link) | | `link` | string | project's `web_url` | Override the card title's link target | | `releases` | number | — | Embed the latest N releases. Absent or `≤ 0` — not fetched, not rendered | | `commits` | number | — | Embed the latest N commits. Absent or `≤ 0` — not fetched, not rendered | diff --git a/examples/site/docs/components/project-info.mdx b/examples/site/docs/components/project-info.mdx index 1553f19..a94523a 100644 --- a/examples/site/docs/components/project-info.mdx +++ b/examples/site/docs/components/project-info.mdx @@ -30,6 +30,7 @@ rendered) when its count prop is a positive number. |---|---|---|---| | `project` | `string \| number` | — | **Required.** Project path (`group/sub/repo`) or numeric ID. | | `showStats` | `boolean` | `true` | Show the stars / forks / last-activity row. | +| `showLinks` | `boolean` | `true` | Link the release / commit / issue items. Set `false` to render them as plain text; the card title stays a link either way. | | `link` | `string` | project's `web_url` | Override the card title's link target. | | `releases` | `number` | — | Embed the latest N releases. Absent or `≤ 0` ⇒ not fetched, not rendered. | | `commits` | `number` | — | Embed the latest N commits. Absent or `≤ 0` ⇒ not fetched, not rendered. | diff --git a/src/components/GitlabProjectInfo.test.tsx b/src/components/GitlabProjectInfo.test.tsx index a3b9186..9485b45 100644 --- a/src/components/GitlabProjectInfo.test.tsx +++ b/src/components/GitlabProjectInfo.test.tsx @@ -69,6 +69,31 @@ describe("GitlabProjectInfo", () => { expect(path?.textContent).toBe("g/r"); }); + it("links commit, issue, and release items by default", () => { + render(); + expect(screen.getByRole("link", { name: "a1b2c3d" })).toHaveAttribute("href", "https://gitlab.com/c/a1b2c3d"); + expect(screen.getByRole("link", { name: /Bug/ })).toHaveAttribute("href", "https://gitlab.com/i/42"); + expect(screen.getByRole("link", { name: /First/ })).toHaveAttribute("href", "https://gitlab.com/r/v1.0"); + }); + + it("renders section items as plain text when showLinks is false, keeping the title link", () => { + render(); + const links = screen.getAllByRole("link"); + expect(links).toHaveLength(1); + expect(links[0]).toHaveTextContent("My Repo"); + expect(screen.getByText("a1b2c3d")).toBeInTheDocument(); + expect(screen.getByText(/#42 Bug/)).toBeInTheDocument(); + expect(screen.getByText("First")).toBeInTheDocument(); + }); + it("renders no section blocks when arrays are absent", () => { const { container } = render(); expect(container.querySelector(".gitlab-section")).toBeNull(); diff --git a/src/components/GitlabProjectInfo.tsx b/src/components/GitlabProjectInfo.tsx index 38e841f..6e49a0f 100644 --- a/src/components/GitlabProjectInfo.tsx +++ b/src/components/GitlabProjectInfo.tsx @@ -9,13 +9,30 @@ type SectionLayout = "list" | "cards"; interface ProjectInfoProps extends ComponentPayload { showStats?: boolean; + showLinks?: boolean; link?: string; releasesLayout?: SectionLayout; commitsLayout?: SectionLayout; issuesLayout?: SectionLayout; } -function Releases({ items, layout }: { items: ReleaseData[]; layout: SectionLayout }) { +/** Render `text` as a link to `href`, or as plain text when links are disabled. */ +function MaybeLink({ + href, + className, + showLinks, + children, +}: { + href?: string; + className: string; + showLinks: boolean; + children: React.ReactNode; +}) { + if (showLinks && href) return {children}; + return {children}; +} + +function Releases({ items, layout, showLinks }: { items: ReleaseData[]; layout: SectionLayout; showLinks: boolean }) { return (
    Releases
    @@ -23,7 +40,7 @@ function Releases({ items, layout }: { items: ReleaseData[]; layout: SectionLayo {items.map((r) => (
  • {r.tagName} - {r.name || r.tagName} + {r.name || r.tagName} {timeAgo(r.releasedAt)}
  • ))} @@ -32,14 +49,14 @@ function Releases({ items, layout }: { items: ReleaseData[]; layout: SectionLayo ); } -function Commits({ items, layout }: { items: CommitData[]; layout: SectionLayout }) { +function Commits({ items, layout, showLinks }: { items: CommitData[]; layout: SectionLayout; showLinks: boolean }) { return (
    Latest commits
      {items.map((c) => (
    • - {c.shortId} + {c.shortId} {c.title} · {c.authorName} {timeAgo(c.createdAt)} @@ -50,14 +67,14 @@ function Commits({ items, layout }: { items: CommitData[]; layout: SectionLayout ); } -function Issues({ items, layout }: { items: IssueData[]; layout: SectionLayout }) { +function Issues({ items, layout, showLinks }: { items: IssueData[]; layout: SectionLayout; showLinks: boolean }) { return (
      Issues
        {items.map((i) => (
      • - #{i.iid} {i.title} + #{i.iid} {i.title} {layout === "cards" && ( · {i.state} · {i.authorName} )} @@ -73,6 +90,7 @@ export function GitlabProjectInfo({ data, error, showStats = true, + showLinks = true, link, releasesLayout = "list", commitsLayout = "list", @@ -96,16 +114,6 @@ export function GitlabProjectInfo({ dangerouslySetInnerHTML={{ __html: data.descriptionHtml }} /> )} -
        {data.path}
        - {data.releases && data.releases.length > 0 && ( - - )} - {data.commits && data.commits.length > 0 && ( - - )} - {data.issues && data.issues.length > 0 && ( - - )} {data.topics.length > 0 && (
        {data.topics.map((t) => ( @@ -113,6 +121,16 @@ export function GitlabProjectInfo({ ))}
        )} +
        {data.path}
        + {data.releases && data.releases.length > 0 && ( + + )} + {data.commits && data.commits.length > 0 && ( + + )} + {data.issues && data.issues.length > 0 && ( + + )} {showStats && (
        ★ {formatCount(data.starCount)} diff --git a/src/gitlab/fetchers.test.ts b/src/gitlab/fetchers.test.ts index af7ffb7..08a1284 100644 --- a/src/gitlab/fetchers.test.ts +++ b/src/gitlab/fetchers.test.ts @@ -213,12 +213,14 @@ describe("fetchReleases", () => { const client = { getReleases: vi.fn(async () => [ { name: "v1", tag_name: "v1", released_at: "2026-01-01T00:00:00Z", description: "**notes**", - upcoming_release: false, assets: { links: [{ name: "bin", url: "https://x/bin" }] } }, + upcoming_release: false, assets: { links: [{ name: "bin", url: "https://x/bin" }] }, + _links: { self: "https://gitlab.com/g/r/-/releases/v1" } }, ]), }; const data = await fetchReleases(ctx(client), { project: "g/r", limit: 5, includePrereleases: true }); expect(data).toHaveLength(1); expect(data[0].tagName).toBe("v1"); + expect(data[0].webUrl).toBe("https://gitlab.com/g/r/-/releases/v1"); expect(data[0].descriptionHtml).toContain("notes"); expect(data[0].assets).toEqual([{ name: "bin", url: "https://x/bin" }]); expect(client.getReleases).toHaveBeenCalledWith("g/r", 5); diff --git a/src/gitlab/fetchers.ts b/src/gitlab/fetchers.ts index 6e4ae49..6e8d24c 100644 --- a/src/gitlab/fetchers.ts +++ b/src/gitlab/fetchers.ts @@ -152,6 +152,7 @@ export async function fetchReleases(ctx: GitLabContext, attrs: Attrs): Promise ({ name: l.name, url: l.url })), + webUrl: r._links?.self, })), ); }); diff --git a/src/gitlab/types.ts b/src/gitlab/types.ts index 234131d..041e54b 100644 --- a/src/gitlab/types.ts +++ b/src/gitlab/types.ts @@ -35,6 +35,8 @@ export interface ReleaseData { descriptionHtml: string; upcomingRelease: boolean; assets: ReleaseAsset[]; + /** Release page URL (GitLab `_links.self`); absent if the API omits it. */ + webUrl?: string; } export interface IssueData { From 6094d80a751caf9fba94288dd9ac83adabbdfe41 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 16:25:22 +0200 Subject: [PATCH 24/28] feat: created date in stats; use absolute human dates (drop relative 'x ago') Signed-off-by: Thomas Decaux --- README.md | 6 ++--- .../site/docs/components/project-info.mdx | 5 ++-- src/components/GitlabProjectInfo.test.tsx | 19 +++++++++----- src/components/GitlabProjectInfo.tsx | 15 ++++++----- src/components/formatDate.test.ts | 15 +++++++++++ src/components/formatDate.ts | 11 ++++++++ src/components/timeAgo.test.ts | 25 ------------------- src/components/timeAgo.ts | 19 -------------- src/gitlab/fetchers.test.ts | 4 ++- src/gitlab/fetchers.ts | 1 + src/gitlab/types.ts | 1 + 11 files changed, 59 insertions(+), 62 deletions(-) create mode 100644 src/components/formatDate.test.ts create mode 100644 src/components/formatDate.ts delete mode 100644 src/components/timeAgo.test.ts delete mode 100644 src/components/timeAgo.ts diff --git a/README.md b/README.md index 87694a9..a201229 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ opt-in and only fetched when its count prop is a positive number. | Prop | Type | Default | Description | |---|---|---|---| | `project` | string \| number | — | **Required.** Project path or ID | -| `showStats` | boolean | `true` | Show stars/forks/last-activity row | +| `showStats` | boolean | `true` | Show the stars / forks / created / last-activity row | | `showLinks` | boolean | `true` | Link the release / commit / issue items. Set `false` to render them as plain text (the card title stays a link) | | `link` | string | project's `web_url` | Override the card title's link target | | `releases` | number | — | Embed the latest N releases. Absent or `≤ 0` — not fetched, not rendered | @@ -118,8 +118,8 @@ opt-in and only fetched when its count prop is a positive number. > Each section's `list` layout renders one compact line per item — release: > tag + name; commit: linked short SHA + title + author; issue: linked `#iid` > -> - title. Every item shows a relative `"x ago"` date pinned to the right. -> `cards` renders a richer variant of the same data. +> - title. Every item shows its date (absolute, e.g. `May 1, 2020`) pinned to +> the right. `cards` renders a richer variant of the same data. > > The `showStats` row can also show extra pills — total commit count, > contributor count, open issue count, repository size — automatically, diff --git a/examples/site/docs/components/project-info.mdx b/examples/site/docs/components/project-info.mdx index a94523a..1aa5ea6 100644 --- a/examples/site/docs/components/project-info.mdx +++ b/examples/site/docs/components/project-info.mdx @@ -29,7 +29,7 @@ rendered) when its count prop is a positive number. | Prop | Type | Default | Description | |---|---|---|---| | `project` | `string \| number` | — | **Required.** Project path (`group/sub/repo`) or numeric ID. | -| `showStats` | `boolean` | `true` | Show the stars / forks / last-activity row. | +| `showStats` | `boolean` | `true` | Show the stars / forks / created / last-activity row. | | `showLinks` | `boolean` | `true` | Link the release / commit / issue items. Set `false` to render them as plain text; the card title stays a link either way. | | `link` | `string` | project's `web_url` | Override the card title's link target. | | `releases` | `number` | — | Embed the latest N releases. Absent or `≤ 0` ⇒ not fetched, not rendered. | @@ -47,7 +47,8 @@ rendered) when its count prop is a positive number. and `issues` may be set, in any order. - `*Layout` props default to `"list"` — a compact single line per item: release = tag + name; commit = linked short SHA + title + author; issue = - linked `#iid` + title. Every item shows a relative `"x ago"` date pinned to + linked `#iid` + title. Every item shows its date (absolute, e.g. + `May 1, 2020` — not "x ago", so a page built once stays correct) pinned to the right. `"cards"` renders a richer variant of the same data. An invalid layout value (anything other than `"list"` or `"cards"`) fails the build. - On a failed fetch in non-strict mode, a fallback notice is rendered instead. diff --git a/src/components/GitlabProjectInfo.test.tsx b/src/components/GitlabProjectInfo.test.tsx index 9485b45..f552a3e 100644 --- a/src/components/GitlabProjectInfo.test.tsx +++ b/src/components/GitlabProjectInfo.test.tsx @@ -4,7 +4,8 @@ import { GitlabProjectInfo } from "./GitlabProjectInfo"; const data = { id: 1, path: "g/r", name: "My Repo", descriptionHtml: "

        A thing

        ", webUrl: "https://gitlab.com/g/r", - starCount: 12, forksCount: 3, topics: ["docs", "tooling"], lastActivityAt: "2026-01-01T00:00:00Z", avatarUrl: null, + starCount: 12, forksCount: 3, topics: ["docs", "tooling"], + createdAt: "2020-05-01T00:00:00Z", lastActivityAt: "2026-01-01T00:00:00Z", avatarUrl: null, }; describe("GitlabProjectInfo", () => { @@ -94,6 +95,11 @@ describe("GitlabProjectInfo", () => { expect(screen.getByText("First")).toBeInTheDocument(); }); + it("shows a created date in the stats row", () => { + render(); + expect(screen.getByText(/^created /)).toBeInTheDocument(); + }); + it("renders no section blocks when arrays are absent", () => { const { container } = render(); expect(container.querySelector(".gitlab-section")).toBeNull(); @@ -117,15 +123,16 @@ describe("GitlabProjectInfo", () => { expect(screen.getByText(/Ada/)).toBeInTheDocument(); }); - it("renders a relative 'ago' date on each release, commit, and issue", () => { + it("renders an absolute (not relative) date on each release, commit, and issue", () => { const { container } = render(); for (const section of ["releases", "commits", "issues"]) { const date = container.querySelector(`.gitlab-section-${section} .gitlab-section-date`); - expect(date?.textContent).toMatch(/ago/); + expect(date?.textContent).toMatch(/2020/); + expect(date?.textContent).not.toMatch(/ago/); } }); diff --git a/src/components/GitlabProjectInfo.tsx b/src/components/GitlabProjectInfo.tsx index 6e49a0f..369dd59 100644 --- a/src/components/GitlabProjectInfo.tsx +++ b/src/components/GitlabProjectInfo.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Fallback } from "./Fallback.js"; import { formatCount } from "./format.js"; import { formatBytes } from "./formatBytes.js"; -import { timeAgo } from "./timeAgo.js"; +import { formatDate } from "./formatDate.js"; import type { ComponentPayload, ProjectInfoData, ReleaseData, CommitData, IssueData } from "./types.js"; type SectionLayout = "list" | "cards"; @@ -41,7 +41,7 @@ function Releases({ items, layout, showLinks }: { items: ReleaseData[]; layout:
      • {r.tagName} {r.name || r.tagName} - {timeAgo(r.releasedAt)} + {formatDate(r.releasedAt)}
      • ))}
      @@ -59,7 +59,7 @@ function Commits({ items, layout, showLinks }: { items: CommitData[]; layout: Se {c.shortId} {c.title} · {c.authorName} - {timeAgo(c.createdAt)} + {formatDate(c.createdAt)}
    • ))}
    @@ -78,7 +78,7 @@ function Issues({ items, layout, showLinks }: { items: IssueData[]; layout: Sect {layout === "cards" && ( · {i.state} · {i.authorName} )} - {timeAgo(i.createdAt)} + {formatDate(i.createdAt)} ))} @@ -110,7 +110,7 @@ export function GitlabProjectInfo({
    {data.descriptionHtml && (
    )} @@ -147,7 +147,10 @@ export function GitlabProjectInfo({ {typeof data.repositorySize === "number" && ( ▤ {formatBytes(data.repositorySize)} )} - updated {new Date(data.lastActivityAt).toLocaleDateString()} + {data.createdAt && ( + created {formatDate(data.createdAt)} + )} + updated {formatDate(data.lastActivityAt)}
    )}
    diff --git a/src/components/formatDate.test.ts b/src/components/formatDate.test.ts new file mode 100644 index 0000000..9b35497 --- /dev/null +++ b/src/components/formatDate.test.ts @@ -0,0 +1,15 @@ +import { describe, it, expect } from "vitest"; +import { formatDate } from "./formatDate.js"; + +describe("formatDate", () => { + it("formats an ISO date in a human, absolute form", () => { + expect(formatDate("2020-05-01T12:00:00Z", "en-US")).toBe("May 1, 2020"); + expect(formatDate("2026-12-25T12:00:00Z", "en-US")).toBe("Dec 25, 2026"); + }); + + it("does not echo the raw ISO string or a relative form", () => { + const out = formatDate("2020-05-01T12:00:00Z", "en-US"); + expect(out).not.toContain("T00:00:00Z"); + expect(out).not.toMatch(/ago/); + }); +}); diff --git a/src/components/formatDate.ts b/src/components/formatDate.ts new file mode 100644 index 0000000..f1004f7 --- /dev/null +++ b/src/components/formatDate.ts @@ -0,0 +1,11 @@ +/** + * Human, static-safe date: "May 1, 2020". Absolute (not "x ago") so a page + * built once stays correct for years. Uses the build host's locale by default. + */ +export function formatDate(iso: string, locale?: string | string[]): string { + return new Date(iso).toLocaleDateString(locale, { + year: "numeric", + month: "short", + day: "numeric", + }); +} diff --git a/src/components/timeAgo.test.ts b/src/components/timeAgo.test.ts deleted file mode 100644 index 084b013..0000000 --- a/src/components/timeAgo.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { timeAgo } from "./timeAgo.js"; - -const now = Date.parse("2026-07-08T00:00:00Z"); - -describe("timeAgo", () => { - it("returns 'just now' for the current instant and the future", () => { - expect(timeAgo("2026-07-08T00:00:00Z", now)).toBe("just now"); - expect(timeAgo("2026-07-09T00:00:00Z", now)).toBe("just now"); - }); - - it("formats minutes and hours", () => { - expect(timeAgo("2026-07-07T23:59:00Z", now)).toBe("1 minute ago"); - expect(timeAgo("2026-07-07T23:00:00Z", now)).toBe("1 hour ago"); - expect(timeAgo("2026-07-07T21:00:00Z", now)).toBe("3 hours ago"); - }); - - it("formats days, weeks, months, and years with pluralization", () => { - expect(timeAgo("2026-07-07T00:00:00Z", now)).toBe("1 day ago"); - expect(timeAgo("2026-07-01T00:00:00Z", now)).toBe("1 week ago"); - expect(timeAgo("2026-05-08T00:00:00Z", now)).toBe("2 months ago"); - expect(timeAgo("2025-07-08T00:00:00Z", now)).toBe("1 year ago"); - expect(timeAgo("2024-07-08T00:00:00Z", now)).toBe("2 years ago"); - }); -}); diff --git a/src/components/timeAgo.ts b/src/components/timeAgo.ts deleted file mode 100644 index b0b29f6..0000000 --- a/src/components/timeAgo.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** Humanize a past timestamp relative to `now`: "3 hours ago", "2 months ago". */ -export function timeAgo(iso: string, now: number = Date.now()): string { - const secs = Math.max(0, Math.round((now - new Date(iso).getTime()) / 1000)); - const units: [number, string][] = [ - [31536000, "year"], - [2592000, "month"], - [604800, "week"], - [86400, "day"], - [3600, "hour"], - [60, "minute"], - ]; - for (const [size, label] of units) { - if (secs >= size) { - const n = Math.floor(secs / size); - return `${n} ${label}${n === 1 ? "" : "s"} ago`; - } - } - return "just now"; -} diff --git a/src/gitlab/fetchers.test.ts b/src/gitlab/fetchers.test.ts index 08a1284..1b2fe68 100644 --- a/src/gitlab/fetchers.test.ts +++ b/src/gitlab/fetchers.test.ts @@ -41,7 +41,8 @@ describe("fetchProjectInfo", () => { const client = { getProject: vi.fn(async () => ({ id: 7, path_with_namespace: "g/r", name: "r", description: "d", web_url: "https://gitlab.com/g/r", - star_count: 3, forks_count: 1, topics: [], last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, + star_count: 3, forks_count: 1, topics: [], created_at: "2020-05-01T00:00:00Z", + last_activity_at: "2026-01-01T00:00:00Z", avatar_url: null, issues_enabled: true, open_issues_count: 12, statistics: { commit_count: 1200, repository_size: 4404019 }, })), @@ -50,6 +51,7 @@ describe("fetchProjectInfo", () => { const c = ctx(client); const data = await fetchProjectInfo(c, { project: "g/r" }); expect(client.getProject).toHaveBeenCalledWith("g/r", { statistics: true }); + expect(data.createdAt).toBe("2020-05-01T00:00:00Z"); expect(data.commitCount).toBe(1200); expect(data.repositorySize).toBe(4404019); expect(data.openIssuesCount).toBe(12); diff --git a/src/gitlab/fetchers.ts b/src/gitlab/fetchers.ts index 6e8d24c..d27c5bb 100644 --- a/src/gitlab/fetchers.ts +++ b/src/gitlab/fetchers.ts @@ -123,6 +123,7 @@ export async function fetchProjectInfo(ctx: GitLabContext, attrs: Attrs): Promis starCount: p.star_count, forksCount: p.forks_count, topics: p.topics ?? [], + createdAt: p.created_at, lastActivityAt: p.last_activity_at, avatarUrl, }; diff --git a/src/gitlab/types.ts b/src/gitlab/types.ts index 041e54b..f2f8692 100644 --- a/src/gitlab/types.ts +++ b/src/gitlab/types.ts @@ -12,6 +12,7 @@ export interface ProjectInfoData { starCount: number; forksCount: number; topics: string[]; + createdAt: string; lastActivityAt: string; avatarUrl: string | null; releases?: ReleaseData[]; From 480898bd0bd552e43c1fa679bb71ef0bac17a8f6 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Wed, 8 Jul 2026 16:26:52 +0200 Subject: [PATCH 25/28] docs: fix mangled ProjectInfo blockquote Signed-off-by: Thomas Decaux --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a201229..31b6279 100644 --- a/README.md +++ b/README.md @@ -116,10 +116,9 @@ opt-in and only fetched when its count prop is a positive number. | `issuesLayout` | `"list"` \| `"cards"` | `"list"` | Layout for the issues section. An invalid value fails the build | > Each section's `list` layout renders one compact line per item — release: -> tag + name; commit: linked short SHA + title + author; issue: linked `#iid` -> -> - title. Every item shows its date (absolute, e.g. `May 1, 2020`) pinned to -> the right. `cards` renders a richer variant of the same data. +> tag and name; commit: linked short SHA, title, and author; issue: linked +> `#iid` and title. Every item shows its date (absolute, e.g. `May 1, 2020`) +> pinned to the right. `cards` renders a richer variant of the same data. > > The `showStats` row can also show extra pills — total commit count, > contributor count, open issue count, repository size — automatically, From 2d9e57075b200d75ad0fba6c6dbd1658355ac71c Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Thu, 9 Jul 2026 10:40:07 +0200 Subject: [PATCH 26/28] chore: doc Signed-off-by: Thomas Decaux --- README.md | 2 ++ docs/screenshot1.png | Bin 0 -> 163675 bytes examples/gitlab/docs/gitlab-runner.md | 3 +++ 3 files changed, 5 insertions(+) create mode 100644 docs/screenshot1.png create mode 100644 examples/gitlab/docs/gitlab-runner.md diff --git a/README.md b/README.md index 31b6279..b5e3b3b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Embed **GitLab** resources — project info, README, releases, issues, and any file or code snippet — directly in your **Docusaurus 3** documentation using MDX components. +![Screenshot](./docs/screenshot1.png) + All data is fetched **at build time** and baked into your static site. No API tokens or network calls ever reach the browser, and pages stay fast. diff --git a/docs/screenshot1.png b/docs/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..34805ac11c57f14075828de870cd08aaf5833d1d GIT binary patch literal 163675 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

    c9PyQjlpHf!H~Tp(BwRW60pQvUG}VjeuJ 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

    Y+=Pu*fUtkT&vDYiasoOSpfA)aO@Bm zq(x|1nh#RxP=gL`6`fynxT(@hGu~W5c@3bno0-3pr#gptvFtf=!E*^g2qwoWf)IKG zSRIHfHVIxAeh)Xd!G1e|Y0{AIJ-yH>E?m77o~aUjr%3WW7q|S#M}lFX3ya>OlG-Lq zlC6Db=hxH*r7Y$hDK;s6ni&p@cZJg@5hgx|ei{o61!O|DEop56i->#)Yn(^O;6gw8 zVw;ra5|Y{W&GhIu!HyzGI7~&ZaVQML_^DqfWcO5{gmru$%cDb!X!5gfGoFvnep?@@bJ(`_W+MejMg_k%{Kw3o%FZWyv;V#OF?gTa@@ zoKnn3UNunK8UjrXYXZ66^J?+ha%}@eKF^}=*@be*JyXs2G7I<}Ln=d?9zxu=Qv-9} zK|!H?@Z=_Q7jW&EmwE2ple*-hcQ2`+F%91~R9*90DtGk-9(8#wrRWqnIgo+|tw{Kr zV9#^ODp}iR*ZD@9-nf#@BJGNf+3cmHU*`QA;|uaAaBs+^9sY2MU|`k6QWbO&7LJ}Z z`>L65-1_|}DteTz3DH)`RMH<>)jPyMr>V|4B>z;4T{7^dBD^mQec8z|U8^oIRzN}m zb-ZU5DR+ZPBk9w0*JD5vv^tWjxF(ub3U`PJ->0BHUB~m9;hOqEg7u1BDx#<;N3Z#? z2h`tJLTc7C-=SS>k@HcbF3ce{_@tUMArpt261^@(VQQIsFtf$l`C*ax1kc>9(xPN> z94wDMn^qI_FdpsWx0oQ{j>_e4UaM%28Gd!4w8!rlIo*K-v|6gKa&FLhsors$INoGJ zC%wyrhBAja=S<2KYBlGb55ZP`~j(ygE?vcDoS3eSGL-2vNl_UBHRyV+?t zSF(Y#IJ45W{^+>m(9U-M4YVk=g`fz7qx|BocF|Xe1ghr{3dQ)WntEETy)(mGy^AP# zs?{e3BR(YIJO>6VoeVmUr$z%0I*DX(4)BsIjwN&e+LjK>>j}fFRtF03n!dJi+LagS zI_cWJr$>ywxj;0vz)SbH|r!=?6iQG;llG9t49B1f%xnt*wZ}8WNre|XE(b? zKi$nfiQ{cDF2s)nK2l?ox?xA+xl2y6Ftk-xbr=-E!eYddy4Dy3fUHw>{2|L%NbljE z&DjY|QVNdiGN=KF{rneio&o6fsA`D!Vs%0Ad0&mUA;q}Io9LnmZjaAQs0=|UAR^tt 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ĐauM2~W9=F6pCngUpxbv|_#umn(t=!BlF0gz)c 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

    ?O|*?E&Mh!+(UuG)7c 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 C@v~(B literal 0 HcmV?d00001 diff --git a/examples/gitlab/docs/gitlab-runner.md b/examples/gitlab/docs/gitlab-runner.md new file mode 100644 index 0000000..04fbf86 --- /dev/null +++ b/examples/gitlab/docs/gitlab-runner.md @@ -0,0 +1,3 @@ + + + \ No newline at end of file From c07da50a0171f7ac942824ccb5807bdd61b737f5 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Thu, 9 Jul 2026 11:00:51 +0200 Subject: [PATCH 27/28] chore: design Signed-off-by: Thomas Decaux --- src/components/GitlabProjectInfo.tsx | 3 +-- theme.css | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/GitlabProjectInfo.tsx b/src/components/GitlabProjectInfo.tsx index 369dd59..58a0230 100644 --- a/src/components/GitlabProjectInfo.tsx +++ b/src/components/GitlabProjectInfo.tsx @@ -148,9 +148,8 @@ export function GitlabProjectInfo({ ▤ {formatBytes(data.repositorySize)} )} {data.createdAt && ( - created {formatDate(data.createdAt)} + created {formatDate(data.createdAt)} - updated {formatDate(data.lastActivityAt)} )} - updated {formatDate(data.lastActivityAt)}

    )}
    diff --git a/theme.css b/theme.css index 594c3b9..ef1b7dc 100644 --- a/theme.css +++ b/theme.css @@ -11,7 +11,7 @@ .gitlab-card { border: 1px solid var(--ifm-color-emphasis-200); border-radius: 10px; - padding: 1rem; + padding: 0.5rem; margin: 1rem 0; background: var(--ifm-background-surface-color); box-shadow: 0 1px 2px rgb(0 0 0 / 0.06), 0 2px 8px rgb(0 0 0 / 0.04); @@ -41,20 +41,26 @@ .gitlab-muted { color: var(--ifm-color-emphasis-600); } + +.gitlab-description p { + margin: 0; +} + .gitlab-stats { display: flex; gap: 1rem; margin-top: 1rem; border-top: 1px solid lightgray; padding-top: 0.5rem; + font-size: 0.8rem; } /* Badges: topics, tags, labels, release assets */ .gitlab-badge { display: inline-block; - padding: 0 0.5rem; + padding: 0 0.4rem; margin-right: 0.25rem; - border-radius: 999px; + border-radius: 5px; background: var(--ifm-color-emphasis-100); color: var(--ifm-color-primary); font-size: 0.85em; @@ -120,6 +126,10 @@ color: var(--ifm-color-emphasis-700); } +.gitlab-description { + margin-bottom: 0.1rem; +} + /* Issues list */ .gitlab-issues { list-style: none; From 6a288ea1ba3820368f7f9e092edc74bfe2f17bbd Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Thu, 9 Jul 2026 11:03:20 +0200 Subject: [PATCH 28/28] chore: claude Signed-off-by: Thomas Decaux --- CLAUDE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 31e468e..4bf7ea8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -133,3 +133,9 @@ Rules: - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). + +## Gitlab API + +You are familiar with Gitlab API: + +OpenAPI: