Compare commits

...
153 Commits
Author SHA1 Message Date
martin-mfg 4340024a9a minor comment improvements 2026-08-27 09:37:15 +02:00
martin-mfg 92238f4dfd test splitting, add logs, small docs changes 2026-08-26 21:08:25 +02:00
martin-mfg b6ef01b6c6 adapt to merged docs 2026-08-26 18:39:33 +02:00
martin-mfg a21e14fa32 Merge remote-tracking branch 'origin/master' into contributed-to 2026-08-26 16:49:25 +02:00
MartinandMarco Pasqualetti 3bf8e6d9d6 update frontend docs (#515)
- add documentation for the [new GitHub light and dark
themes](https://github.com/stats-organization/github-stats-extended/pull/480),
for the [support light & dark mode in one
card](https://github.com/stats-organization/github-stats-extended/pull/430)
PR and for the [new "Total Contributions"
stat](https://github.com/stats-organization/github-stats-extended/pull/482)
- use dynamic light/dark cards in the docs at /frontend/docs

---------

Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2026-08-26 15:24:58 +02:00
martin-mfg 9ef1554064 tests and docs 2026-08-25 13:12:16 +02:00
martin-mfg bd58cfd47c add includeOwnRepos param, wire up params 2026-08-25 10:41:39 +02:00
martin-mfg c2c5fa34d8 Merge branch 'master' into contributed-to 2026-08-25 07:10:56 +02:00
Marco Pasqualetti 01f2468417 feat(core): convert wakatime card to TypeScript (#505)
Another step of #140

- `cards/wakatime.js` ➡️ `cards/wakatime.ts`
- Test and snapshot renamed to `renderWakatimeCard.test.ts` /
`renderWakatimeCard.test.ts.snap`;
  the snapshot are unchanged.
- New `common/languageColors.ts` owns the language-color table:
the gist and wakatime cards repeated `(languageColors as Record<string,
string>)[name] || "#858585"` three times, and top-languages declared a
fourth copy of that hex.
They now share `getLanguageColor()` / `DEFAULT_LANG_COLOR`, and typing
the table at the import removes the cast.
2026-08-24 21:06:11 +02:00
Marco Pasqualetti 8c99381676 chore(frontend): remove vercel.json and orphan Dockerfile (#506)
- Closes #504
2026-08-24 20:36:09 +02:00
Martin 8c78a825b5 intermediate fix for docs (#507) 2026-08-23 20:55:20 +02:00
Marco Pasqualettiandmartin-mfg e0de81e061 feat: publish the documentation and the card wizard from one Astro site (#489)
- Closes #461 

---

- Turns `apps/frontend` into an [Astro](https://astro.build/) +
[Starlight](https://starlight.astro.build/) site.
Serves the docs at `/frontend/docs` and the card wizard at `/frontend`:
one dev server, one build, one deploy, and no Python in
`vercel-preparation.sh` or in the local setup.
- The wizard becomes a page of that site: a React island on Starlight's
`splash` layout,
  so it inherits the header, footer and search.
  Its own app bar, theme picker and Redux theme slice go away.
Starlight writes the `data-theme` daisyUI already reads, so one control
themes both halves.
- The `*.md` sources move to `apps/frontend/src/content/docs/docs/` with
a `title` in
frontmatter and Starlight's `:::note` asides, which lets Astro resolve
their links and
  images natively (no markdown-conversion code of our own).
`starlight-links-validator` then fails the build on a dead internal
link.
- `packages/core/src/themes/README.md` is generated into the site
instead; README and CONTRIBUTING links follow the move.
- Tailwind now shares a page with Starlight, so `index.css` declares the
cascade-layer order
(its utilities must outrank Starlight's reset) and scopes the app's
element rules under
  `.wizard`, leaving the site identical on both halves.

---

To try it out just run

```sh
pnpm --filter ./apps/frontend run dev
```

---

| Wizard | Documentation |
|--------|--------|
| <img width="1710" height="666" alt="Screenshot 2026-08-17 at 00 54 18"
src="https://github.com/user-attachments/assets/c5b29e34-5464-41f3-9d01-08f593c87660"
/> | <img width="1710" height="720" alt="Screenshot 2026-08-17 at 00 54
25"
src="https://github.com/user-attachments/assets/e7e3f414-bf62-441d-9f6e-537a20044b0b"
/> |

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-08-23 14:09:40 +02:00
martin-mfg 682f9ef566 WIP: improve new code 2026-08-23 11:08:37 +02:00
martin-mfg d61a76f88d WIP: all-time contributed to 2026-08-22 18:21:20 +02:00
Martin 3fb7c8bce5 contributions mock data, dependabot schedule (#500)
* add missing mock data for new contributions stat to frontend
* schedule dependabot twice a week
2026-08-20 10:28:36 +02:00
Marco Pasqualetti 31d28cdb1c fix: keep tests out of the build output and disable tsc emit by default (#486)
- `packages/core/tsconfig.json` covers `tests` but also set `outDir:
"build"`,
so a simple `tsc` emitted `build/tests`, which turbo then cached and
replayed on every later build.
- `tsconfig.base.json` now sets `noEmit: true`;
`packages/core/tsconfig.build.json` is the only config that opts back
in, and it takes the `outDir`.
- Removed `noEmit` from the typecheck and scripts configs.
- Removed `composite: true` from both apps: nothing references them as
TypeScript projects.

> [!NOTE]
> Local-only issue: CI never runs a standalone `tsc` and starts from a
clean checkout,
>  so the polluted `build/` never showed up there.
2026-08-16 23:14:07 +02:00
Marco Pasqualetti 56e0bb66cb test(core): use jest-dom assertions for DOM checks (#485)
- Card tests now assert with jest-dom matchers instead of reading nodes
by hand:
`toHaveTextContent`, `toHaveAttribute`, `toHaveStyle`,
`toBeInTheDocument`.
- Fixes assertions in `renderStatsCard.test.ts`: 
  `queryByTestId` returns `null`, so `toBeDefined()` could never fail. 
One of them was checking `rank-percentile-text`, which is a CSS class
and not a test id.
- Index-existence checks became `toHaveLength(n)`; 
- The five `toMatchInlineSnapshot` translation checks became explicit
`toHaveTextContent` with the same strings.
- `renderWakatimeCard.test.js` moved to the `screen` API
- `card.test.ts` uses `querySelector` over `getElementsBy*`.
2026-08-16 21:26:31 +02:00
3ab6cf918b add "Total Contributions" stat (#482)
Add a new optional stat for the stats card, "Total Contributions".

* I think this is something many users want. There are many issues where
people wonder why their "contributed to" or their "total commits"
doesn't match their contributions.
* This PR also introduces a way to fetch and sum stats which are only
available for 1 year at a time in the GitHub API. We could reuse this
later for more highly requested all-time stats if necessary.

I will add documentation in a separate PR.

---------

Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2026-08-16 17:28:03 +02:00
MartinandMarco Pasqualetti 480c26b717 add GitHub themes (#480)
This is only the code part of
https://github.com/stats-organization/github-stats-extended/pull/457.
Documentation from that PR will be merged later, as explained there.

---------

Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2026-08-14 23:29:02 +02:00
Marco Pasqualettiandmartin-mfg c86492f9c2 feat(core): generate TypeScript types from the GraphQL queries (#475)
The queries moved to `src/graphql/queries/*.graphql` and their types are
generated from
GitHub's schema instead of hand-written.

- `scripts/generate-graphql-types.js` validates each query against
`@octokit/graphql-schema` and writes one file per query plus a shared
`common.ts`,
  so a wrong field fails generation and a wrong variable fails `tsc`.
- `pnpm check-graphql-types` runs in CI, so the generated types can't
drift.
- `httpGraphQLRequest` takes a generated document: the four
`*QueryResponse` interfaces are gone and `GRAPHQL_REPOS_FIELD` is a real
fragment.
Untyped `request` stays exported for the backend, with a TODO for
considering its removal.
- Fixed nullability the hand-written types got wrong
  - (a gist's `owner` and `files[].name`, `user`, `repositories.nodes`);
  - `Lang.color` is nullable again
- `parseOwnerAffiliations` returns the schema's `RepositoryAffiliation`.
- `graphql` stays on 16: `@octokit/graphql-schema` needs `^16`.

> [!WARNING]
> The four `apps/backend` contract snapshots were refreshed:
> the recorded request text changed, the rendered SVGs did not.

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-08-14 16:18:43 +02:00
Martin c177bd9ec5 top-langs: adapt test code to prod code (#476)
follow-up to
https://github.com/stats-organization/github-stats-extended/pull/473

Instead of adapting our prod code's signatures for our test code, I
think we should adapt our test code here.
2026-08-12 20:33:47 +02:00
Marco Pasqualetti 944857b2a5 feat(core): convert top languages card to TypeScript (#473)
Another step of #140.

- `src/cards/top-languages.js` + its test ➡️ `.ts`. 
- `fetchers/types.ts`: `Lang.color` made `string | null` to match
GitHub's nullable GraphQL `Language.color`.
The card already falls back to a default and a test pass `color: null`.
- Extracted the copy-pasted "resolve + validate language color" block
into `resolveLangColor`.
- Test uses the `screen` API, caches repeated `queryAll` results in
local variables, and types its language fixtures with `satisfies
TopLangData`.
2026-08-12 18:53:51 +02:00
Marco Pasqualetti a9cfee5809 feat(core): convert stats card to TypeScript (#466)
Another step of #140.

- `src/cards/stats.js` + its test ➡️ `.ts`.
- Moved `createTextNode` out of the stats card into `common/render.js`
(next to the other SVG-node builders).
  It was the one card export used by another card (repo).
- `I18n` gained a default type param (usable as a bare type),
and `Card`/`createTextNode` accept the possibly-undefined options cards
forward.
- Card tests now use the `screen` API instead of
`getByTestId(document.body, …)`.
See [recommended query
entrypoint](https://testing-library.com/docs/queries/about/#screen) and
cache repeated queries in local variables.
  Also applied on the already-migrated `gist`/`repo` tests.
2026-08-10 12:20:16 +02:00
MartinandMarco Pasqualetti 4edf85bdc2 downgrade pnpm to version 10 (#459)
Upgrading pnpm to version 11 led to errors on Vercel on each request.
https://github.com/stats-organization/github-stats-extended/issues/458
shows an example stack trace. Vercel doesn't support pnpm 11 yet
according to [their docs](https://vercel.com/docs/package-managers). I
have tried understanding and fixing the actual issue, but had to give up
at some point. So for now let's downgrade to pnpm 10, wait for Vercel to
support pnpm 11, then upgrade again.

Additionally, #458 showed that we should direct users to use the
`release` branch when self-hosting. So this PR also updates the docs
accordingly.

> [!NOTE]
When this PR is approved I should create an issue for the pnpm 11
upgrade.

---------

Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2026-08-08 09:13:52 +02:00
Marco Pasqualetti 1abd91eace chore: upgrade to pnpm 11 (#450)
- Bumps `packageManager` to `pnpm@11.20.0` and migrates
`onlyBuiltDependencies` to the new `allowBuilds` map.
- Adds
  - `savePrefix: ""`
  - `strictPeerDependencies`
  - `engineStrict`

  to `pnpm-workspace.yaml`.
  Nothing fails today, so these only guard regressions.

- Regenerates the lockfile and dedupes.
  Collapses two duplicates:
  - `typescript-eslint` 8.64.0 → 8.65.0,
- `vite>lightningcss` override, guarded by a new `lint:deps` check so
the override can't silently drift.
  
- **axios stays on 1.18.1.** 1.19 adds a params generic that breaks 5
core tests typecheck
(https://github.com/stats-organization/github-stats-extended/pull/449).
   Worth fixing in its own PR then.
2026-08-06 21:53:49 +02:00
Marco Pasqualetti dd28696356 feat(core): convert repo card to TypeScript (#447)
Another step of #140

- `src/cards/repo.js` + its test ➡️ `.ts`
- Colocated each card's options type into its own file; 
`types.ts` keeps only the shared `CommonOptions` and the options of the
js cards.
All `*CardOptions` are now `interface CardNameOptions extends
CommonOptions` (were `type` intersections).
- `fetchers/types.ts`: made `RepoInfo.description` and `primaryLanguage`
(and its fields) nullable to match the GraphQL schema.
  The card already guards for these and the tests exercise them.
- `common/I18n.ts`: widened the constructor's `locale?: string` to
accept `undefined`, since cards forward possibly-undefined query
options.
2026-08-05 22:57:14 +02:00
github-actions[bot]andmartin-mfg 5d3d3af4da Update languages colors (#446)
The
[generate-language-colors](https://github.com/stats-organization/github-stats-extended/actions/workflows/generate-language-colors.yml)
workflow detected changes in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)
and regenerated the local language colors JSON file.

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-08-05 16:01:23 +02:00
Martin 5156202340 fix update-langs, align naming (#440)
Like #433 for the update-langs workflow.
* rename the workflow, script, and pnpm command for consistency
* fix workflow which
[broke](https://github.com/stats-organization/github-stats-extended/actions/workflows/update-langs.yml)
after upgrading js-yaml a month ago
* reduced workflow permissions
2026-08-05 12:28:19 +02:00
Marco Pasqualetti 28bb41a488 feat(core): convert gist card to TypeScript (#443)
Another step of #140, continues #426. Starts `cards/`.

- `src/cards/types.d.ts` ➡️ `types.ts`. 
- `gist` card + test ➡️ `.ts`. Typed against `GistData` /
`Partial<GistCardOptions>`;
replaced the `// @ts-ignore` on the language-color lookup with a
`Record` cast.
- `Card.ts`: widened the constructor's `border_radius?: number` to `?:
number | undefined`:
cards forward possibly-undefined query options (same pattern as
`getCardColors`).
  Needed by every card.
2026-08-04 16:32:42 +02:00
MartinandMarco Pasqualetti 3ff8364844 docs: format md files with prettier - extended (#444)
Built on top of
https://github.com/stats-organization/github-stats-extended/pull/441 to
manually format two html blocks - see 2nd commit in this PR. I pushed
the suggested changes via a commit instead of using GitHub's "suggested
changes" feature so that we can preview the changes rendered by GitHub
[here](https://github.com/stats-organization/github-stats-extended/blob/prettier-md-2/docs/advanced_documentation.md).

closes #441 
(let's see if this works)

### Description from #441

- Followup to
https://github.com/stats-organization/github-stats-extended/pull/432#discussion_r3698998568

---

- Removed `*.md` from `.prettierignore` and reformatted all Markdown
files.
- `generate-theme-readme.js` now formats its output (root config via
`resolveConfig`) so `src/themes/README.md` stays formatted.

---------

Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2026-08-04 15:56:06 +02:00
Marco Pasqualetti df66b24c6a ci(generate-theme-readme): align theme readme generation with TypeScript sources (#433)
Follow-up to [the `themes/index.js` ➡️ `index.ts`
migration](https://github.com/stats-organization/github-stats-extended/pull/139),
which left the theme README generation pipeline pointing to files that
no longer exist.

* Renamed the workflow, script, and pnpm command to the consistent
`generate-theme-readme` (previously `generate-theme-doc` /
`theme-readme-gen`) and updated them to use the `.ts` source.
* Reduced workflow permissions from 12 declared scopes to the 2 actually
required:
  * `contents: write`
  * `pull-requests: write`
* Fixed the generated PR body, which incorrectly referenced language
files.

---

> [!NOTE]
> After this is merged, I'd consider applying the same permission
reduction and workflow rename to `update-langs.yml`.
2026-08-03 14:34:56 +02:00
Marco Pasqualetti fbd292a03d ci(repeat-recent-requests): replace http-request-action with curl (#434)
- Swaps the third-party `fjogeleit/http-request-action` for a plain
`curl` step in `repeat-recent-requests.yml`:
the workflow only needs a POST with a timeout, so the action added no
value.
- The request carries only what the endpoint reads. `repeat-recent`
checks `req.method` and nothing else.
  So the action's JSON body and headers were never used. 
  Each flag has a one-line comment explaining why it's there.
- Failure semantics are preserved: a `>= 400` status or a connection
failure still fails the job, and the response body and status are now
logged even on failure.

Example run:
https://github.com/marcalexiei/github-stats-extended/actions/runs/30751117550/job/91505220151
2026-08-03 14:15:07 +02:00
Marco Pasqualettiandmartin-mfg 68eddf0f81 docs: explain the Backend E2E failure and refine docs content (#432)
Related to
https://github.com/stats-organization/github-stats-extended/pull/430

> [(The failing Backend E2E test is expected, because this PR changes
the output of most cards, but in a non-visible
way.)](https://github.com/stats-organization/github-stats-extended/pull/430#issuecomment-5153247227)

- `ci.yml`: comment explaining what the job compares and why it fails.
- `CONTRIBUTING.md`: new "Tests" section, there was no testing
documentation before.
Removed the FAQ, whose three entries were all user questions, and
promoted Feature Request from `h3` to `h2`.
- `advanced_documentation`: kept the two useful FAQ answers, in context:
- language-card troubleshooting links under "Language stats algorithm",
- percent-encoding example (`&hide=jupyter%20notebook`) under "Hide
individual languages".
Demo labels changed from list items to `h4` headings: the images were
never indented into the list, so each label rendered as its own one-item
bullet.
- `deploy.md`: normalized the token-setup lists from `*` with 4 space
indents to `-` with 2 space indents.
  Marker and indentation only, nesting preserved.
- `README.md`: the seven section headings were `h1`. With the HTML
`<h1>` in the banner that made eight top-level headings on one page, so
they are now `h2`.
Heading anchors are derived from the text, so the table of contents is
unaffected.

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-08-03 13:46:24 +02:00
MartinandMarco Pasqualetti 58d941d63a support light & dark mode in one card (#430)
Add support for light mode and dark mode without [brittle,
GitHub-specific
approaches](https://github.com/stats-organization/github-stats-extended/blob/master/docs/advanced_documentation.md#responsive-card-theme).

All endpoints accept new parameters: theme_light, theme_dark,
title_color_light,
title_color_dark, icon_color_light, icon_color_dark, etc.

> Priority (lowest -> highest):
>  default theme -> `theme` param -> `theme_light`/`theme_dark` param
>  -> general color params -> `*_light`/`*_dark` color params

Additionally, this PR adds the recently introduced `progBarBgColor`
parameter to `CardColors` and handles it together with the other colors
in color.ts, for consistency. And it adds one test for an archived
repository.

This PR is already too big, so I'll add documentation in a later PR, and
maybe also a simple new option for the frontend.

My main question here is: does the API and the intended behaviour make
sense? Bugs can be fixed later, but once the new parameters are
released, ideally we shouldn't change them anymore.

---------

Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2026-08-03 13:14:13 +02:00
Marco Pasqualetti 372eaaf058 feat(core): align repo star field naming and add type coverage (#429)
Followup of
https://github.com/stats-organization/github-stats-extended/pull/426#discussion_r3695160068

> Now that we have `RepoInfo`, would it make sense to define
`RepositoryData` as combination of `RepoInfo` and `RepoUserStats` (and
align the naming of "starCount"/"stargazerCount") in a later PR?
2026-08-01 21:00:35 +02:00
Marco Pasqualetti 09a72df953 feat(core): convert repo fetcher to TypeScript (#426)
Another step of #140, continues #415. 

Last fetcher: `fetchers/` is now fully TS.

- `repo` fetcher + test ➡️ `.ts`. 
Defines `RepoInfo` and `RepoQueryResponse`, calls
`retryer<RepoQueryResponse>(…)`
- `types.ts`: made `RepositoryData`'s PR/issue count fields optional:
they come from `fetchRepoUserStats`, which only sets them when the
matching `include_*` flag is on.
2026-08-01 10:10:24 +02:00
Marco Pasqualetti 587f4589b8 feat(core): convert stats fetcher to TypeScript (#415)
Another step of #140, continues #410.

- `stats` fetcher + test ➡️ `.ts`, threading typed GraphQL and
REST-search shapes through `retryer<…>`.
- Added a `src/_github-username-regex.d.ts` shim (package ships no
types).
- Fixed a bug caught by the types: statsFetcher expected pat in its
options object,
  but the caller passed it as a second argument, so it was ignored. 
Private-access users' token is now correctly passed to the main stats
query.
2026-07-29 20:05:48 +02:00
Marco Pasqualetti 259fc7c5bc feat(core): convert wakatime + top-languages fetchers to TypeScript (#410)
Another step of #140, continues #370.

- `wakatime` fetcher + test ➡️ `.ts`. Typed the props object and the
axios body (`axios.get<{ data: WakaTimeData }>`);
replaced the raw `err.response.status` access with an
`axios.isAxiosError` guard.
- `top-languages` fetcher + test ➡️ `.ts`, calling
`retryer<TopLanguagesQueryResponse>(…)`.
Split the single `let repoNodes` (reused across three shapes) into typed
values and simplified two of the three `reduce` calls (map-builder →
`for…of`, final sort ➡️ `Object.fromEntries`).
The flatten `reduce` is kept as-is — its order feeds the `repoCount`
logic.
- `types.ts`: added `count: number` to `Lang` (the fetcher always
produces it; the card only reads `name`/`color`/`size`).
2026-07-28 22:46:27 +02:00
Martin caccc4d69a bump core version to 2.1.5 (#407) 2026-07-27 19:20:46 +02:00
Martin 1c6b4c31d9 fix dropdown bugs and gradient bug (#402)
* In the frontend, "Modify Parameters" step, a few options of several
dropdowns encoded multiple parameters in their value string. That didn't
work anymore since a recent change. This PR fixes the affected dropdown
options.
* Since a recent change, using the "ambient_gradient" theme for WakaTime
card didn't work anymore. This PR fixes that.
2026-07-26 12:13:26 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2992f63de2 ci(deps): Bump fjogeleit/http-request-action from 2.0.0 to 2.0.1 (#362)
Bumps
[fjogeleit/http-request-action](https://github.com/fjogeleit/http-request-action)
from 2.0.0 to 2.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/fjogeleit/http-request-action/releases">fjogeleit/http-request-action's
releases</a>.</em></p>
<blockquote>
<h2>v2.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Bump axios from 1.13.4 to 1.13.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/209">fjogeleit/http-request-action#209</a></li>
<li>Bump axios from 1.13.5 to 1.13.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/210">fjogeleit/http-request-action#210</a></li>
<li>Bump undici from 6.23.0 to 6.24.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/211">fjogeleit/http-request-action#211</a></li>
<li>Bump axios from 1.13.6 to 1.14.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/212">fjogeleit/http-request-action#212</a></li>
<li>Revert &quot;Bump axios from 1.13.6 to 1.14.0&quot; by <a
href="https://github.com/fjogeleit"><code>@​fjogeleit</code></a> in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/213">fjogeleit/http-request-action#213</a></li>
<li>docs(readme): fix inaccuracies, heading hierarchy, and document
output by <a
href="https://github.com/kranthipoturaju"><code>@​kranthipoturaju</code></a>
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/222">fjogeleit/http-request-action#222</a></li>
<li>docs(readme): update redirecting GitHub Docs URLs by <a
href="https://github.com/kranthipoturaju"><code>@​kranthipoturaju</code></a>
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/224">fjogeleit/http-request-action#224</a></li>
<li>docs: fix inaccuracies and document missing outputs in action.yml by
<a
href="https://github.com/kranthipoturaju"><code>@​kranthipoturaju</code></a>
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/223">fjogeleit/http-request-action#223</a></li>
<li>Bump actions/checkout from 6.0.2 to 6.0.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/227">fjogeleit/http-request-action#227</a></li>
<li>Bump axios from 1.13.6 to 1.15.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/216">fjogeleit/http-request-action#216</a></li>
<li>Bump axios from 1.15.0 to 1.17.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/225">fjogeleit/http-request-action#225</a></li>
<li>Bump follow-redirects from 1.15.11 to 1.16.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/217">fjogeleit/http-request-action#217</a></li>
<li>Bump nick-fields/assert-action from 2.0.0 to 3.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/214">fjogeleit/http-request-action#214</a></li>
<li>fix [DEP0169] DeprecationWarning: <code>url.parse()</code> behavior
is not standardized... by <a
href="https://github.com/wvidana"><code>@​wvidana</code></a> in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/232">fjogeleit/http-request-action#232</a></li>
<li>Bump undici from 6.24.0 to 6.27.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/231">fjogeleit/http-request-action#231</a></li>
<li>Bump axios from 1.17.0 to 1.18.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/229">fjogeleit/http-request-action#229</a></li>
<li>Bump axios from 1.18.0 to 1.18.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/233">fjogeleit/http-request-action#233</a></li>
<li>Bump <code>@​vercel/ncc</code> from 0.38.4 to 0.44.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/230">fjogeleit/http-request-action#230</a></li>
<li>Bump form-data from 4.0.5 to 4.0.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/228">fjogeleit/http-request-action#228</a></li>
<li>Bump nick-fields/assert-action from 3.0.0 to 4.0.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/234">fjogeleit/http-request-action#234</a></li>
<li>Bump actions/checkout from 6.0.3 to 7.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/235">fjogeleit/http-request-action#235</a></li>
<li>chore(dep): dependency updates by <a
href="https://github.com/fjogeleit"><code>@​fjogeleit</code></a> in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/236">fjogeleit/http-request-action#236</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/kranthipoturaju"><code>@​kranthipoturaju</code></a>
made their first contribution in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/222">fjogeleit/http-request-action#222</a></li>
<li><a href="https://github.com/wvidana"><code>@​wvidana</code></a> made
their first contribution in <a
href="https://redirect.github.com/fjogeleit/http-request-action/pull/232">fjogeleit/http-request-action#232</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/fjogeleit/http-request-action/compare/v2.0...v2.0.1">https://github.com/fjogeleit/http-request-action/compare/v2.0...v2.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/f98bb26551cd1af7d3eb2674578a80754ece88db"><code>f98bb26</code></a>
chore(dep): dependency updates (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/236">#236</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/3584789bbffe004d18e05cd2779aa75a3e2bf69a"><code>3584789</code></a>
Bump actions/checkout from 6.0.3 to 7.0.0 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/235">#235</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/1d6a1ee0f4e72dcbc37a910236b8a2fedf1221df"><code>1d6a1ee</code></a>
Bump nick-fields/assert-action from 3.0.0 to 4.0.1 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/234">#234</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/5d99b6683aaaf873571876966005dd6fc8b3136a"><code>5d99b66</code></a>
Bump form-data from 4.0.5 to 4.0.6 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/228">#228</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/a53287a79a36cc186797c68df701ae69978eb2e8"><code>a53287a</code></a>
Bump <code>@​vercel/ncc</code> from 0.38.4 to 0.44.1 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/230">#230</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/7540a773d6694bdd6235eb1e25bb01723b7855fc"><code>7540a77</code></a>
Bump axios from 1.18.0 to 1.18.1 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/233">#233</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/55dcde0359495be61b69697dab6a9e2f310d4ffd"><code>55dcde0</code></a>
Bump axios from 1.17.0 to 1.18.0 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/229">#229</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/d703d5a26d1c679a77adda062ad23653f804dea3"><code>d703d5a</code></a>
Bump undici from 6.24.0 to 6.27.0 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/231">#231</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/0b8c6ccce4d482d1466df75f3ac0f9b0e2183474"><code>0b8c6cc</code></a>
Using the globally scoped URLSearchParams from the WHATWG URL API (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/232">#232</a>)</li>
<li><a
href="https://github.com/fjogeleit/http-request-action/commit/ed82ab41da4893d38edab6b5fad057b285078493"><code>ed82ab4</code></a>
Bump nick-fields/assert-action from 2.0.0 to 3.0.0 (<a
href="https://redirect.github.com/fjogeleit/http-request-action/issues/214">#214</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/fjogeleit/http-request-action/compare/551353b829c3646756b2ec2b3694f819d7957495...f98bb26551cd1af7d3eb2674578a80754ece88db">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=fjogeleit/http-request-action&package-manager=github_actions&previous-version=2.0.0&new-version=2.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 13:35:51 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9201b65d82 ci(deps): Bump actions/checkout from 7.0.0 to 7.0.1 (#395)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0
to 7.0.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>skip running unsafe pr check if input is default by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2518">actions/checkout#2518</a></li>
<li>trim only ascii whitespace for branch by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2521">actions/checkout#2521</a></li>
<li>escape values passed to --unset by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2530">actions/checkout#2530</a></li>
<li>Various dependency updates</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v7...v7.0.1">https://github.com/actions/checkout/compare/v7...v7.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.1</h2>
<ul>
<li>Skip running unsafe pr check if input is default by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2518">actions/checkout#2518</a></li>
<li>Trim only ascii whitespace for branch by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2521">actions/checkout#2521</a></li>
<li>Escape values passed to --unset by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2530">actions/checkout#2530</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/3d3c42e5aac5ba805825da76410c181273ba90b1"><code>3d3c42e</code></a>
prep v7.0.1 release (<a
href="https://redirect.github.com/actions/checkout/issues/2531">#2531</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/28802689a136bfcdb721715abd713740beecbe07"><code>2880268</code></a>
escape values passed to --unset (<a
href="https://redirect.github.com/actions/checkout/issues/2530">#2530</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/12cd2235efa0937479335606d7c3ac9f6c0973b1"><code>12cd223</code></a>
trim only ascii whitespace for branch (<a
href="https://redirect.github.com/actions/checkout/issues/2521">#2521</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/62661c4e71a304b2823ed026347b8d34c3eac541"><code>62661c4</code></a>
skip running unsafe pr check if input is default (<a
href="https://redirect.github.com/actions/checkout/issues/2518">#2518</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/e8d4307400f9427dba7cb98e488d6ab85f1cec5f"><code>e8d4307</code></a>
Bump the minor-actions-dependencies group with 2 updates (<a
href="https://redirect.github.com/actions/checkout/issues/2499">#2499</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/631c942040754b6e095e929c1677c07e10ed4f87"><code>631c942</code></a>
eslint 9 (<a
href="https://redirect.github.com/actions/checkout/issues/2474">#2474</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/4f1f4aec02e41874fa0262ea8ff5172d7978ad1e"><code>4f1f4ae</code></a>
Bump actions/upload-artifact from 4 to 7 (<a
href="https://redirect.github.com/actions/checkout/issues/2476">#2476</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/ba097532fb203f7e88c9c3c0b899b49469908a92"><code>ba09753</code></a>
Bump actions/checkout from 6 to 7 (<a
href="https://redirect.github.com/actions/checkout/issues/2488">#2488</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/b9e0990d219a03df7633c93f6f005a8fecbcab22"><code>b9e0990</code></a>
Bump docker/login-action from 3.3.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2479">#2479</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/e8cb398be4a550817e382abf69e4c12c76fce1f2"><code>e8cb398</code></a>
Bump docker/build-push-action from 6.5.0 to 7.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2478">#2478</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=7.0.0&new-version=7.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 13:35:23 +02:00
Martin 90508f02f3 upgrade several dependencies (#400) 2026-07-25 13:27:54 +02:00
Martin 3bda3abee7 bump core version to 2.1.4 (#399) 2026-07-24 20:28:47 +02:00
Martin 8f7cc5824b longer prerender caching 2 (#393)
Try to further reduce load on Vercel. ISR expiration time is now 3 day
minus 1 hour, and I will set `UPDATE_AFTER_HOURS` to 3 days.
2026-07-23 21:34:27 +02:00
Martin 1d7ea960f5 switch from stargazers.totalCount to stargazerCount (#391) 2026-07-23 20:30:54 +02:00
shuaixr 2b908ad146 fix: update Vercel deploy link to point to the backend directory (#382)
Fixes #381
2026-07-23 14:23:45 +02:00
Marco Pasqualetti 63ca91e1b1 feat(core): start fetchers TypeScript migration (retryer generic + gist) (#370)
Another step of #140, continues #322. 
Starts migrating `fetchers/`.

- `retryer` is now generic over the response payload:
`retryer<TData>(…)` returns `AxiosResponse<TData & ResponseErrors>` so
callers get their own `response.data` shape while the retryer still
reads `errors`/`message`.
  Defaults to `unknown` so existing callers are unaffected.
- `src/fetchers/types.d.ts` ➡️ `types.ts` (references are extensionless,
so they resolve unchanged).
- `gist` fetcher + test ➡️ `.ts`, using `retryer<GistQueryResponse>(…)`.

> [!WARNING]
> Consider reviewing #375 first, as it resolves the "Resource limits for
this query exceeded" issue.
> This PR will likely conflict with it, so it's better to merge #375
first.
2026-07-23 09:50:09 +02:00
Martin 2007f51dc0 small test improvements (#386) 2026-07-22 20:45:36 +02:00
Martin 3258ad09b0 longer prerender caching (#360)
Load on Vercel is higher than expected. It seems the problem is that
Vercel's CDN evicts our responses before they go stale, because there
are relatively few requests per card. So the repeat-recent functionality
currently causes load without achieving much.

This PR increases ISR expiration time to see how this affects load in
practice. Setting this time via env var might be a task for later.
2026-07-13 10:24:28 +02:00
MartinandMarco Pasqualetti 4ccec4604b fix warnings (#342)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2026-07-12 19:51:53 +02:00
Marco Pasqualetti b81abe07c4 refactor(frontend): consolidate Home card options into a single state object (#344)
`HomeScreen` held ~16 separate `useState` hooks for card options and
threaded them through 34 props into `CustomizeStage`.
This consolidates them into a single typed `CardOptions` state object
(new `pages/Home/cardOptions.ts`), updated via a keyed setter.

### Changes

- `CustomizeStage` props go from 34 to 5; `buildCardUrl` takes `userId`,
`selectedCard` and `options`.
- Stage descriptions move into `STAGE_LABELS`, replacing a positional
string array in the JSX.
- The three GitHub-URL paste handlers in `CustomizeStage` share one
`pastedPathTail` helper.
- A failed OAuth `authenticate` call left the page stuck on the loading
spinner;
the exchange now runs in `try/catch/finally` and the redirect is parsed
with
  `URLSearchParams` instead of substring matching.
2026-07-12 17:49:10 +02:00
Marco Pasqualetti b0100ca48e fix(core): validate username / gist id against the safe-character pattern (#356)
- Related to #342 

`stats` and `pin` already reject `username`/`repo`/`owner` that don't
match `safePattern` (`/^[-\w/.,]+$/`), but `top-langs`, `wakatime`, and
`gist` didn't.

This applies the same guard:

- `top-langs` + `wakatime`: reject a `username` containing unsafe
characters.
Matters most for `wakatime`, where `username` is interpolated into the
outbound request URL path.
- `gist`: same guard on its `id` param.

All three return the existing `error - permanent` card before any fetch.

## Additional change

Simplified the `layout` / `stats_format` checks in `top-langs`: 
dropped the redundant `typeof x !== "string" ||` (a non-string already
fails `.includes(...)`),
keeping the `x !== undefined` guard so the params stay optional.
2026-07-12 14:23:40 +02:00
Shaurya Sharmaandmartin-mfg 7c7fca420b Fix link to calculateRank source file in docs (#351)
The advanced documentation file in the docs/ folder incorrectly links to
a nonexistent folder called "backend/" for the calculate rank logic.
This PR fixes the broken link by changing the link to
"packages/core/src/calculateRank.js".

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-07-11 12:48:00 +02:00
Martin 1d9db07ed4 fix color of theme switcher (#343)
The latest daisyui upgrade changed the theme switcher's color in light
mode. This PR reverts that change.
2026-07-10 19:49:41 +02:00
Martin 4043488ee0 adjust repeat-recent (#341)
* GitHub actually triggers the cron workflow every few hours. Because
they are overloaded I suppose. This PR is an attempt to give the
workflow more chances to be triggered.
* Sometimes we run into secondary rate limiting of the GitHub API when
repeating recent requests. Having less workers should fix this.
2026-07-07 16:21:48 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 807aa453d8 ci(deps): Bump actions/checkout from 6.0.3 to 7.0.0 (#299)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3
to 7.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>block checking out fork pr for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
<li>getting ready for checkout v7 release by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2464">actions/checkout#2464</a></li>
<li>update error wording by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2467">actions/checkout#2467</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.3...v7.0.0">https://github.com/actions/checkout/compare/v6.0.3...v7.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"><code>9c091bb</code></a>
update error wording (<a
href="https://redirect.github.com/actions/checkout/issues/2467">#2467</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/1044a6dea927916f2c38ba5aeffbc0a847b1221a"><code>1044a6d</code></a>
getting ready for checkout v7 release (<a
href="https://redirect.github.com/actions/checkout/issues/2464">#2464</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/f0282184c7ce73ab54c7e4ab5a617122602e575f"><code>f028218</code></a>
Bump the minor-npm-dependencies group across 1 directory with 3 updates
(<a
href="https://redirect.github.com/actions/checkout/issues/2462">#2462</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/d914b262ffc244530a203ab40decab34c3abf34d"><code>d914b26</code></a>
upgrade module to esm and update dependencies (<a
href="https://redirect.github.com/actions/checkout/issues/2463">#2463</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/537c7ef99cef6e5ddb5e7ff5d16d14510503801d"><code>537c7ef</code></a>
Bump <code>@​actions/core</code> and <code>@​actions/tool-cache</code>
and Remove uuid (<a
href="https://redirect.github.com/actions/checkout/issues/2459">#2459</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/130a169078a413d3a5246a393625e8e742f387f6"><code>130a169</code></a>
Bump js-yaml from 4.1.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2461">#2461</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/7d09575332117a40b46e5e020664df234cd416f3"><code>7d09575</code></a>
Bump flatted from 3.3.1 to 3.4.2 (<a
href="https://redirect.github.com/actions/checkout/issues/2460">#2460</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/0f9f3aa320cb53abeb534aeb54048075d9697a0e"><code>0f9f3aa</code></a>
Bump actions/publish-immutable-action (<a
href="https://redirect.github.com/actions/checkout/issues/2458">#2458</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/f9e715a95fcd1f9253f77dd28f11e88d2d6460c7"><code>f9e715a</code></a>
block checking out fork pr for pull_request_target and workflow_run (<a
href="https://redirect.github.com/actions/checkout/issues/2454">#2454</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6.0.3&new-version=7.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-06 09:34:52 +02:00
github-actions[bot]andmartin-mfg 6f1ad4b732 Update languages JSON (#321)
The
[update-langs](https://github.com/stats-organization/github-stats-extended/actions/workflows/update-langs.yml)
action found new/updated languages in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml).

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-07-06 09:33:34 +02:00
Martin 536199bf4a several dependency upgrades again (#336) 2026-07-06 09:25:04 +02:00
Martin a2bb443789 several dependency upgrades (#334) 2026-07-06 08:24:06 +02:00
Marco Pasqualetti 5da671b171 ci: install only chromium for Playwright e2e (#329)
The frontend Playwright config only enables the chromium project, so CI
was needlessly downloading Firefox and WebKit.
Scope `playwright install` to `chromium` to speed up the e2e job.
2026-07-05 09:53:08 +02:00
Marco Pasqualettiandmartin-mfg ed9d284973 feat(frontend): build card URLs with a typed per-card builder (#328)
Card URLs were built by hand-concatenating paths and query strings in
several places.
This replaces that with a typed, immutable per-card URL builder
(`cardUrl(cardType)`), so URLs are assembled in one place with proper
encoding.

## Changes

- New `models/CardUrl.ts`: one builder per card type, exposing only the
params that card supports.
`URLSearchParams` handles joining + encoding. Also carries a per-card
`filename()` for downloads.
- `getFullSuffix.ts` ➡️ `buildCardUrl.ts`: returns a builder instead of
a string.
- The builder is passed as a prop through all card components; no
hand-written `?`/`&` concatenation left.
- New `useCardDescriptor` hook resolves per-card display metadata (guest
hint + link) and owns the
Gist URL fetch, replacing the repeated `switch (selectedCard)` blocks in
`Home`.
- Added tests for `CardUrl`.

### Notes

- Params now go through `URLSearchParams`, so commas become `%2C` and
spaces `%20`.
  The server decodes these before use, so rendered cards are unchanged.
- Builders are `useMemo`'d in `Home` to keep stable references.

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-07-05 09:48:23 +02:00
Marco Pasqualettiandmartin-mfg 748d8c5430 refactor(frontend): replace eslint react plugins with @eslint-react/eslint-plugin (#326)
- Fixes #325

`eslint-plugin-react@7.37.5` doesn't declare support for ESLint 10,
producing an unmet-peer-dependency warning.

## Changes

- Swapped `eslint-plugin-react` ➡️ `@eslint-react/eslint-plugin` and
adopted its `recommended-typescript` config.
This also replaces `eslint-plugin-react-hooks` (the preset ships
equivalent `rules-of-hooks`/`exhaustive-deps`), so both old plugins are
removed.
- Fixed the violations the new preset surfaced:
  - IIFEs-in-JSX in `Home.tsx` ➡️ lifted to `const` bindings.
  - Ref naming (`*Ref`).
- 4 `set-state-in-effect` cases ➡️ rewritten as render-phase state
adjustment ([React
docs](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)).
- Extracted the shared debounced-input logic from
`TextSection`/`NumericSection` into a `useDebouncedField` hook.
- Added a standalone Playwright spec (`e2e/app-trends-auth.spec.ts`)
covering the `AppTrends` auth-driven stage transition.

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-07-03 16:27:36 +02:00
Marco Pasqualetti fef8e90c86 feat(core): migrate common/ render, ops, http, calculateRank to TypeScript (#322)
Second wave of the `packages/core` TypeScript migration (#140),
continuing #292.
Converts the remaining `common/` modules and their tests from JSDoc
`.js` to real `.ts`.

## Changes

- **Modules ➡️ `.ts`**: `common/http`, `common/ops`, `common/render`,
`calculateRank`.
- **Tests ➡️ `.ts`**: `ops`, `flexLayout`, `render`, `calculateRank`.
- **`src/_emoji-name-map.d.ts`**: `declare module` shim.
`emoji-name-map` ships no types
and has no `@types` package (same as the existing
`@uppercod/css-to-object` shim).
- **`common/color.ts`**: widened `getCardColors` optional params to `?:
string | undefined`
so `renderError` can forward possibly-`undefined` colors under
`exactOptionalPropertyTypes`.
2026-07-03 16:26:23 +02:00
Martin fa82216dfe fix primary color in step 4 (#317) 2026-06-30 09:04:48 +02:00
Marco Pasqualettiandmartin-mfg ef3f207dbf feat(frontend): add light/dark theming with theme picker (#300)
Adds light/dark theming to the frontend using the existing DaisyUI +
Tailwind setup.

- Theme picker dropdown in the header; selection is stored in Redux and
persisted to `localStorage`.
The toggle and each option show a sun/moon icon (shared `ThemeIcon`
component) so light/dark are easy to tell apart.
- Migrated hardcoded colors to DaisyUI tokens so the whole UI (and
toasts) follow the theme.
- `Button` now uses `variant` / `size` / `outline` props instead of raw
`btn-*` classes.
- Theme preview cards: (Select Theme grid and the final Display card)
sit on a two-tone backdrop (light or dark), chosen by the card theme so
previews stay consistent regardless of the app theme.
- Smooth theme-switch transitions: surfaces fade gently while
interactive elements stay snappy; respects `prefers-reduced-motion`.
- Added an e2e test for the picker.


<details><summary>Step 1</summary>
<p>


<img width="1710" height="811" alt="Screenshot 2026-06-24 at 06 54 40"
src="https://github.com/user-attachments/assets/38d629b5-eaa3-4fb1-b976-c56e7a840ced"
/>

<img width="1706" height="808" alt="Screenshot 2026-06-24 at 06 54 32"
src="https://github.com/user-attachments/assets/3811a574-016c-4a93-82a9-a340519f5257"
/>


</p>
</details> 

<details><summary>Step 4</summary>
<p>


<img width="1709" height="899" alt="Screenshot 2026-06-24 at 06 56 22"
src="https://github.com/user-attachments/assets/4053b938-1faa-427b-b621-e79ce7b2c7b1"
/>

<img width="1705" height="894" alt="Screenshot 2026-06-24 at 06 55 41"
src="https://github.com/user-attachments/assets/8aace78a-f1ca-4d29-b57e-938f7b473e67"
/>

</p>
</details>

---------

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-06-29 10:04:00 +02:00
Marco Pasqualetti 35fbec8004 feat(core): begin TypeScript migration of common modules (#292)
Starts migrating `packages/core` to TypeScript (#140).

- Converted the `common/` modules to `.ts`: 
- `Card`, `I18n`, `color`, `config`, `constants`, `error`, `fmt`,
`html`, `icons`, `retryer`, `translations`.
- Replaced JSDoc types and `@ts-check`/`@ts-ignore` comments with real
TypeScript types.
- Enabled type-checking on the source and moved `noCheck` into the build
config.
- Converted the tests that target those modules to `.ts`: 
   - `fmt`, `i18n`, `card`, `color`, `html`, `retryer`.
- Converted `tests/_setup.ts` and moved the jest-dom matcher typing
there: augmenting vitest's `Matchers` interface (per the vitest docs) so
matchers like `toHaveAttribute` are typed on `expect(...)`.
- Added a `tests/css-to-object.d.ts` shim so `@uppercod/css-to-object`
resolves under `nodenext` (it ships types but doesn't expose them via
`exports`).
2026-06-22 19:27:14 +02:00
Marco Pasqualetti 3ef27f872c chore(vscode): update prettier extension id (#293)
Update the Prettier extension ID.

Avoids prompting the user to install "Prettier (DO NOT USE)" extension.

<img width="473" height="130" alt="image"
src="https://github.com/user-attachments/assets/ea422ba1-262c-402b-a8f0-cf4cbcbff011"
/>
2026-06-21 10:15:43 +02:00
Martin 5557e9c772 upgrade several actions (#290) 2026-06-19 00:04:12 +02:00
Martin 570fc4aaed bump core version to 2.1.3 (#289) 2026-06-18 23:42:01 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2ac454a177 build(deps-dev): Bump turbo from 2.9.17 to 2.9.18 (#286)
Bumps [turbo](https://github.com/vercel/turborepo) from 2.9.17 to
2.9.18.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vercel/turborepo/releases">turbo's
releases</a>.</em></p>
<blockquote>
<h2>Turborepo v2.9.18</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Changelog</h3>
<ul>
<li>release(turborepo): 2.9.17 by <a
href="https://github.com/github-actions"><code>@​github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/vercel/turborepo/pull/13047">vercel/turborepo#13047</a></li>
<li>ci: Fetch version.txt via API in docs alias failure notification by
<a href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a>
in <a
href="https://redirect.github.com/vercel/turborepo/pull/13050">vercel/turborepo#13050</a></li>
<li>fix: Harden cache archive symlink restore by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13051">vercel/turborepo#13051</a></li>
<li>chore: Remove web UI mode by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13052">vercel/turborepo#13052</a></li>
<li>fix: Harden query server file access by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13053">vercel/turborepo#13053</a></li>
<li>fix: Confine prune patch paths by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13054">vercel/turborepo#13054</a></li>
<li>fix: Prevent git argument injection in SCM refs by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13055">vercel/turborepo#13055</a></li>
<li>fix: Strip special mode bits from cache restore by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13056">vercel/turborepo#13056</a></li>
<li>fix: Contain incremental cache outputs by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13057">vercel/turborepo#13057</a></li>
<li>fix(turborepo): Normalize Windows daemon path hash by <a
href="https://github.com/Balance8"><code>@​Balance8</code></a> in <a
href="https://redirect.github.com/vercel/turborepo/pull/13020">vercel/turborepo#13020</a></li>
<li>fix: Preserve vt100 cell byte counts by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13058">vercel/turborepo#13058</a></li>
<li>fix: Separate artifact signature fields by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13059">vercel/turborepo#13059</a></li>
<li>fix: Validate OidHash hex buffers by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13060">vercel/turborepo#13060</a></li>
<li>fix: Block self-hosted login URLs from attempting to use Vercel's
SSO by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/13061">vercel/turborepo#13061</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Balance8"><code>@​Balance8</code></a>
made their first contribution in <a
href="https://redirect.github.com/vercel/turborepo/pull/13020">vercel/turborepo#13020</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/vercel/turborepo/compare/v2.9.17...v2.9.18">https://github.com/vercel/turborepo/compare/v2.9.17...v2.9.18</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/turborepo/commit/3bdce3277d2e61cdbf29f244a515dd4b896d2556"><code>3bdce32</code></a>
publish 2.9.18 to registry</li>
<li><a
href="https://github.com/vercel/turborepo/commit/2a76556b9457c514fa597a09fef23da2e1250c8c"><code>2a76556</code></a>
fix: Block self-hosted login URLs from attempting to use Vercel's SSO
(<a
href="https://redirect.github.com/vercel/turborepo/issues/13061">#13061</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/da8e3487acce10039425180c42875d7ccc484ed5"><code>da8e348</code></a>
fix: Validate OidHash hex buffers (<a
href="https://redirect.github.com/vercel/turborepo/issues/13060">#13060</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/3018717c28fc8f1ecffa2c92e3260be0dc0165aa"><code>3018717</code></a>
fix: Separate artifact signature fields (<a
href="https://redirect.github.com/vercel/turborepo/issues/13059">#13059</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/34514e278822ccbe3a083730b7032709ef16f85c"><code>34514e2</code></a>
fix: Preserve vt100 cell byte counts (<a
href="https://redirect.github.com/vercel/turborepo/issues/13058">#13058</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/24e2d3498f54fa1f8b729f18764f63cb05072bc6"><code>24e2d34</code></a>
fix(turborepo): Normalize Windows daemon path hash (<a
href="https://redirect.github.com/vercel/turborepo/issues/13020">#13020</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/16dc881cabf5c13d02c27a5d7cfdfeb60736206c"><code>16dc881</code></a>
fix: Contain incremental cache outputs (<a
href="https://redirect.github.com/vercel/turborepo/issues/13057">#13057</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/92e1f8e5365ea5a95e47c3698deb2f7adaa55eba"><code>92e1f8e</code></a>
fix: Strip special mode bits from cache restore (<a
href="https://redirect.github.com/vercel/turborepo/issues/13056">#13056</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/f46f896ef414823582131304c9749e9be011fe37"><code>f46f896</code></a>
fix: Prevent git argument injection in SCM refs (<a
href="https://redirect.github.com/vercel/turborepo/issues/13055">#13055</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/7f353ca3a6f28fbbcce931aeab7be0efe718c466"><code>7f353ca</code></a>
fix: Confine prune patch paths (<a
href="https://redirect.github.com/vercel/turborepo/issues/13054">#13054</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/turborepo/compare/v2.9.17...v2.9.18">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=turbo&package-manager=npm_and_yarn&previous-version=2.9.17&new-version=2.9.18)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 10:57:59 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b837c50885 build(deps-dev): Bump @types/node from 24.13.1 to 24.13.2 in the typescript group (#285)
Bumps the typescript group with 1 update:
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).

Updates `@types/node` from 24.13.1 to 24.13.2
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/node&package-manager=npm_and_yarn&previous-version=24.13.1&new-version=24.13.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 10:57:37 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dfb07a69b9 ci(deps): Bump actions/checkout from 6.0.2 to 6.0.3 (#270)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2
to 6.0.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Update changelog by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2357">actions/checkout#2357</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>Update changelog for v6.0.3 by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2446">actions/checkout#2446</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/yaananth"><code>@​yaananth</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.3">https://github.com/actions/checkout/compare/v6...v6.0.3</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/df4cb1c069e1874edd31b4311f1884172cec0e10"><code>df4cb1c</code></a>
Update changelog for v6.0.3 (<a
href="https://redirect.github.com/actions/checkout/issues/2446">#2446</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/1cce3390c2bfda521930d01229c073c7ff920824"><code>1cce339</code></a>
Fix checkout init for SHA-256 repositories (<a
href="https://redirect.github.com/actions/checkout/issues/2439">#2439</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/900f2210b1d28bbbd0bd22d17926b9e224e8f231"><code>900f221</code></a>
fix: expand merge commit SHA regex and add SHA-256 test cases (<a
href="https://redirect.github.com/actions/checkout/issues/2414">#2414</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/0c366fd6a839edf440554fa01a7085ccba70ac98"><code>0c366fd</code></a>
Update changelog (<a
href="https://redirect.github.com/actions/checkout/issues/2357">#2357</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6.0.2&new-version=6.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 22:41:07 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 41b51dcfd9 build(deps-dev): Bump @types/node from 24.13.0 to 24.13.1 in the typescript group across 1 directory (#276)
Bumps the typescript group with 1 update in the / directory:
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).

Updates `@types/node` from 24.13.0 to 24.13.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 22:39:53 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7687c47d85 build(deps-dev): Bump the eslint group across 1 directory with 2 updates (#284)
Bumps the eslint group with 2 updates in the / directory:
[eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) and
[typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).

Updates `eslint-plugin-jsdoc` from 63.0.1 to 63.0.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/gajus/eslint-plugin-jsdoc/releases">eslint-plugin-jsdoc's
releases</a>.</em></p>
<blockquote>
<h2>v63.0.2</h2>
<h2><a
href="https://github.com/gajus/eslint-plugin-jsdoc/compare/v63.0.1...v63.0.2">63.0.2</a>
(2026-06-06)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>allow typedef returns that may be void; fixes <a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1390">#1390</a>
(<a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1699">#1699</a>)
(<a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/319e84b21dfc21213f42998beee26e199660747c">319e84b</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/50a7fbcf4abf7b27ca2774f65397f2f74395ea41"><code>50a7fbc</code></a>
chore: update semver and devDeps.</li>
<li><a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/6041995ba873b5641e3b9bd1f95de004b4b9e7da"><code>6041995</code></a>
docs: fix in output</li>
<li><a
href="https://github.com/gajus/eslint-plugin-jsdoc/commit/319e84b21dfc21213f42998beee26e199660747c"><code>319e84b</code></a>
fix: allow typedef returns that may be void; fixes <a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1390">#1390</a>
(<a
href="https://redirect.github.com/gajus/eslint-plugin-jsdoc/issues/1699">#1699</a>)</li>
<li>See full diff in <a
href="https://github.com/gajus/eslint-plugin-jsdoc/compare/v63.0.1...v63.0.2">compare
view</a></li>
</ul>
</details>
<br />

Updates `typescript-eslint` from 8.60.1 to 8.61.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">typescript-eslint's
releases</a>.</em></p>
<blockquote>
<h2>v8.61.0</h2>
<h2>8.61.0 (2026-06-08)</h2>
<h3>🚀 Features</h3>
<ul>
<li><strong>ast-spec:</strong> change type of
<code>UnaryExpression.prefix</code> to always <code>true</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12372">#12372</a>)</li>
<li><strong>ast-spec:</strong> tighten types of
<code>ArrowFunction</code>, <code>YieldExpression</code>,
<code>TSTypePredicate</code> (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12373">#12373</a>)</li>
</ul>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>rule-schema-to-typescript-types:</strong> respect ECMAScript
line terminators (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12374">#12374</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Kirk Waiblinger <a
href="https://github.com/kirkwaiblinger"><code>@​kirkwaiblinger</code></a></li>
<li>lumir</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.61.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md">typescript-eslint's
changelog</a>.</em></p>
<blockquote>
<h2>8.61.0 (2026-06-08)</h2>
<p>This was a version bump only for typescript-eslint to align it with
other projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.61.0">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/16a5b247affc32af21b695cf96dfd75d7ded50a3"><code>16a5b24</code></a>
chore(release): publish 8.61.0</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.0/packages/typescript-eslint">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 21:01:53 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7917d54fdf build(deps-dev): Bump @types/react from 19.2.16 to 19.2.17 in the react group across 1 directory (#283)
Bumps the react group with 1 update in the / directory:
[@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).

Updates `@types/react` from 19.2.16 to 19.2.17
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/react&package-manager=npm_and_yarn&previous-version=19.2.16&new-version=19.2.17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 21:01:03 +02:00
Martin 7771a34790 several dependency upgrades (#282) 2026-06-17 20:52:00 +02:00
Martin 1c266ad51e dependency upgrades (#265) 2026-06-07 18:24:46 +02:00
Martin 6258d90604 fix non-mocked frontend requests (#263) 2026-06-07 10:04:19 +02:00
Martin 6d857d0f34 upgrade dependencies (#262) 2026-06-06 12:23:41 +02:00
github-actions[bot]andmartin-mfg e15deeac76 Update languages JSON (#254)
The
[update-langs](https://github.com/stats-organization/github-stats-extended/actions/workflows/update-langs.yml)
action found new/updated languages in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml).

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-06-06 09:24:43 +02:00
Martin b5d50fc28f debug playwright (#261) 2026-06-06 09:23:54 +02:00
Marco Pasqualetti 1d394c7fad chore(actions): don’t repeat-recent-requests on forks (#259)
I received a notification about a failed workflow run on my fork of this
repository:

*
https://github.com/marcalexiei/github-stats-extended/actions/runs/26996444955

It looks like the `repeat-recent-requests` workflow is being triggered
on forks, even though it should only run on the canonical repositories.
This PR adds an `if` condition to prevent it from running on forks.

The condition matches the one already used in the `update-languages`
workflow:


https://github.com/stats-organization/github-stats-extended/blob/96d141e8c717f42a1a29e67f64984a19182dd575/.github/workflows/update-langs.yml#L31-L36

---

Additionally, this PR renames `update_tags.yml` to `update-tags.yml` for
consistency with the repository's workflow naming convention.
2026-06-05 19:32:58 +02:00
Martin 76e09ce3b0 improve 'Responsive Card Theme' docs (#260)
- reorder subsections
- shorten `<picture>` code
2026-06-05 18:17:02 +02:00
Martin 96d141e8c7 update documentation, step 2 (#243) 2026-05-21 23:28:09 +02:00
martin-mfg 6310073428 update documentation, step 2 2026-05-21 21:45:14 +02:00
Martin 1d8aaf0fc8 update documentation (#242) 2026-05-21 21:21:31 +02:00
martin-mfg 4a2785e286 update documentation 2026-05-21 21:19:35 +02:00
Martin 16ea9afebe set correct version for 'core' package (#218) 2026-05-08 12:54:54 +02:00
Martin c707e6b7b1 ignore git checks when publishing (#216) 2026-05-08 12:11:03 +02:00
Martin 9dc4910d4b finish setup for npm publishing (#212) 2026-05-08 11:51:02 +02:00
Martin 5d1978011e add npm publishing (#211) 2026-05-07 19:08:16 +02:00
Martin d84cc27b9c fix browser_rendering layout (#208) 2026-05-06 15:25:33 +02:00
Takuma IMAMURAandmartin-mfg e43e55c2b7 feat: replace render-time text wrapping with browser-native wrapping (#191)
This PR uses `foreignObject` with CSS line-clamp to let the browser
handle text wrapping natively instead of manually wrapping on the
server. This provides better font-aware wrapping while keeping
server-side line estimation for SVG height calculation.

Fixes https://github.com/anuraghazra/github-readme-stats/issues/4862.

### Example card

<img width="400" height="150" alt="hyperfinitism-sample-repo"
src="https://github.com/user-attachments/assets/c3a58531-c689-4798-af02-f55ef3a90fa1"
/>

---------

Signed-off-by: Takuma IMAMURA <209989118+hyperfinitism@users.noreply.github.com>
Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-05-06 11:00:11 +02:00
Martin f884df6312 add top-langs customization to fork.md (#205) 2026-05-03 19:27:15 +02:00
github-actions[bot]andmartin-mfg f67dd0f381 Update languages JSON (#203)
The
[update-langs](https://github.com/stats-organization/github-stats-extended/actions/workflows/update-langs.yml)
action found new/updated languages in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml).

Co-authored-by: martin-mfg <2026226+martin-mfg@users.noreply.github.com>
2026-05-03 14:04:12 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1da425c0bb build(deps-dev): Bump knip from 6.6.2 to 6.7.0 (#202)
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip)
from 6.6.2 to 6.7.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/webpro-nl/knip/releases">knip's
releases</a>.</em></p>
<blockquote>
<h2>Release 6.7.0</h2>
<ul>
<li>Fix markdown reporter column width (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1700">#1700</a>)
(4713108de98bcfe76eb606036b9f968abe3e89a0)</li>
<li>Handle Vitest agent and minimal reporters (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1701">#1701</a>)
(a71ead11b99aaa67f31eed7b35628907b2dddd06) - thanks <a
href="https://github.com/dskwrite"><code>@​dskwrite</code></a>!</li>
<li>Add e2e tests w/ tsgo defs + lib consumption
(98113e61d913e47631748b58c25d1042c8dd387e)</li>
<li>Fix pkg name inconsistencies
(544c3e68d72dd754b20f8e0b0bf8ed107c05d677)</li>
<li>Add <code>export {}</code> to maintain module shape
(27d8a0230c10f2827ba1e1a8b2c65020554f61f8)</li>
<li>Strip leading UTF-8 BOM before parsing package.json
(47e4029c39ff95043cd495681e4c7558970b0ed9)</li>
<li>Skip workspace with invalid JSON manifest
(bfb48670966387a3cd26005b9fb9d8769b2a1213)</li>
<li>Preserve out-of-bound writes to stdout/err
(95faad8d6219ba456a3700df697273eb154c1dd9)</li>
<li>Consistent prefix in logError/logWarning
(2c6d8a049d1c20d9427425b4abae3b76ac7a5720)</li>
<li>Don't exit on config file load error
(0914bd3832b1851e4d34137577c0d08efaf8aef6)</li>
<li>Fix tsgo resolution in e2e tests
(a68950131e33214ef2a4f13d11fab1a78c16bad6)</li>
</ul>
<h2>Release 6.6.3</h2>
<ul>
<li>Use venz chart svg directly from CDN
(ba7e9073d1770e4a21ec18435bcb61f394db0d1c)</li>
<li>Bump that plugin counter
(abae75bbd3030e006fe67d3f63641a31a65e671c)</li>
<li>Fix plugin title (f2b8fcfc94bb78fb70dfe2be3f48ef8f254066f7)</li>
<li>Work Table util class: fix bugs, tighten API, water-fill widths
(2eb4045e0f34c2638875504b9b64e881e9a74db5)</li>
<li>Questionable.. (970a0dbdfc7b59df2fd0a3064a4bd6be4e5a59bc)</li>
<li>Fmt (14538ec1fe94cdf3c45798515160609f3ffbec9d)</li>
<li>Ensure output is flushed on exit (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1699">#1699</a>)
(9533365115d4704e9b057b7ba8d5f43fd553d1a1) - thanks <a
href="https://github.com/joshkel"><code>@​joshkel</code></a>!</li>
<li>Use currentColor to support light/dark themes
(7ea055aa68b86ccc6cb302a3092c2da9e56cdde6)</li>
<li>Tighten type-chain alignment + gate via ignoreExportsUsedInFile
(resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1698">#1698</a>)
(0910b33fc6c07867a751af2e26989a61c8a6c548)</li>
<li>Eating our own dog food ↻
(d5aa1f6b007a2dfef1a8326000dc1802510d64ab)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/webpro-nl/knip/commit/ca5545e2a3f262cbb3e9db4705abb4f4264b102f"><code>ca5545e</code></a>
Release knip@6.7.0</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/a68950131e33214ef2a4f13d11fab1a78c16bad6"><code>a689501</code></a>
Fix tsgo resolution in e2e tests</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/0914bd3832b1851e4d34137577c0d08efaf8aef6"><code>0914bd3</code></a>
Don't exit on config file load error</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/2c6d8a049d1c20d9427425b4abae3b76ac7a5720"><code>2c6d8a0</code></a>
Consistent prefix in logError/logWarning</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/95faad8d6219ba456a3700df697273eb154c1dd9"><code>95faad8</code></a>
Preserve out-of-bound writes to stdout/err</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/bfb48670966387a3cd26005b9fb9d8769b2a1213"><code>bfb4867</code></a>
Skip workspace with invalid JSON manifest</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/47e4029c39ff95043cd495681e4c7558970b0ed9"><code>47e4029</code></a>
Strip leading UTF-8 BOM before parsing package.json</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/27d8a0230c10f2827ba1e1a8b2c65020554f61f8"><code>27d8a02</code></a>
Add <code>export {}</code> to maintain module shape</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/544c3e68d72dd754b20f8e0b0bf8ed107c05d677"><code>544c3e6</code></a>
Fix pkg name inconsistencies</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/98113e61d913e47631748b58c25d1042c8dd387e"><code>98113e6</code></a>
Add e2e tests w/ tsgo defs + lib consumption</li>
<li>Additional commits viewable in <a
href="https://github.com/webpro-nl/knip/commits/knip@6.7.0/packages/knip">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=knip&package-manager=npm_and_yarn&previous-version=6.6.2&new-version=6.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-03 13:57:31 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 46df37adb7 build(deps): Bump axios from 1.15.1 to 1.15.2 in the axios group across 1 directory (#195)
Bumps the axios group with 1 update in the / directory:
[axios](https://github.com/axios/axios).

Updates `axios` from 1.15.1 to 1.15.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/releases">axios's
releases</a>.</em></p>
<blockquote>
<h2>v1.15.2</h2>
<p>This release delivers prototype-pollution hardening for the Node HTTP
adapter, adds an opt-in <code>allowedSocketPaths</code> allowlist to
mitigate SSRF via Unix domain sockets, fixes a keep-alive socket memory
leak, and ships supply-chain hardening across CI and security docs.</p>
<h2>🔒 Security Fixes</h2>
<ul>
<li><strong>Prototype Pollution Hardening (HTTP Adapter):</strong>
Hardened the Node HTTP adapter and
<code>resolveConfig</code>/<code>mergeConfig</code>/validator paths to
read only own properties and use null-prototype config objects,
preventing polluted <code>auth</code>, <code>baseURL</code>,
<code>socketPath</code>, <code>beforeRedirect</code>, and
<code>insecureHTTPParser</code> from influencing requests. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10779">#10779</a></strong>)</li>
<li><strong>SSRF via <code>socketPath</code>:</strong> Rejects
non-string <code>socketPath</code> values and adds an opt-in
<code>allowedSocketPaths</code> config option to restrict permitted Unix
domain socket paths, returning <code>AxiosError</code>
<code>ERR_BAD_OPTION_VALUE</code> on mismatch. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10777">#10777</a></strong>)</li>
<li><strong>Supply-chain Hardening:</strong> Added <code>.npmrc</code>
with <code>ignore-scripts=true</code>, lockfile lint CI, non-blocking
reproducible build diff, scoped CODEOWNERS, expanded
<code>SECURITY.md</code>/<code>THREATMODEL.md</code> with provenance
verification (<code>npm audit signatures</code>), 60-day resolution
policy, and maintainer incident-response runbook. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10776">#10776</a></strong>)</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li><strong><code>allowedSocketPaths</code> Config Option:</strong> New
request config option (and TypeScript types) to allowlist Unix domain
socket paths used by the Node http adapter; backwards compatible when
unset. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10777">#10777</a></strong>)</li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li><strong>Keep-alive Socket Memory Leak:</strong> Installs a single
per-socket <code>error</code> listener tracking the active request via
<code>kAxiosSocketListener</code>/<code>kAxiosCurrentReq</code>,
eliminating per-request listener accumulation,
<code>MaxListenersExceededWarning</code>, and linear heap growth under
concurrent or long-running keep-alive workloads (fixes <a
href="https://redirect.github.com/axios/axios/issues/10780">#10780</a>).
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10788">#10788</a></strong>)</li>
</ul>
<h2>🔧 Maintenance &amp; Chores</h2>
<ul>
<li><strong>Changelog:</strong> Updated <code>CHANGELOG.md</code> with
v1.15.1 release notes. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10781">#10781</a></strong>)</li>
</ul>
<p><a
href="https://github.com/axios/axios/compare/v1.15.1...v1.15.2">Full
Changelog</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/blob/v1.x/CHANGELOG.md">axios's
changelog</a>.</em></p>
<blockquote>
<h2>v1.15.2 - April 21, 2026</h2>
<p>This release delivers prototype-pollution hardening for the Node HTTP
adapter, adds an opt-in <code>allowedSocketPaths</code> allowlist to
mitigate SSRF via Unix domain sockets, fixes a keep-alive socket memory
leak, and ships supply-chain hardening across CI and security docs.</p>
<h2>🔒 Security Fixes</h2>
<ul>
<li><strong>Prototype Pollution Hardening (HTTP Adapter):</strong>
Hardened the Node HTTP adapter and
<code>resolveConfig</code>/<code>mergeConfig</code>/validator paths to
read only own properties and use null-prototype config objects,
preventing polluted <code>auth</code>, <code>baseURL</code>,
<code>socketPath</code>, <code>beforeRedirect</code>, and
<code>insecureHTTPParser</code> from influencing requests. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10779">#10779</a></strong>)</li>
<li><strong>SSRF via <code>socketPath</code>:</strong> Rejects
non-string <code>socketPath</code> values and adds an opt-in
<code>allowedSocketPaths</code> config option to restrict permitted Unix
domain socket paths, returning <code>AxiosError</code>
<code>ERR_BAD_OPTION_VALUE</code> on mismatch. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10777">#10777</a></strong>)</li>
<li><strong>Supply-chain Hardening:</strong> Added <code>.npmrc</code>
with <code>ignore-scripts=true</code>, lockfile lint CI, non-blocking
reproducible build diff, scoped CODEOWNERS, expanded
<code>SECURITY.md</code>/<code>THREATMODEL.md</code> with provenance
verification (<code>npm audit signatures</code>), 60-day resolution
policy, and maintainer incident-response runbook. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10776">#10776</a></strong>)</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li><strong><code>allowedSocketPaths</code> Config Option:</strong> New
request config option (and TypeScript types) to allowlist Unix domain
socket paths used by the Node http adapter; backwards compatible when
unset. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10777">#10777</a></strong>)</li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li><strong>Keep-alive Socket Memory Leak:</strong> Installs a single
per-socket <code>error</code> listener tracking the active request via
<code>kAxiosSocketListener</code>/<code>kAxiosCurrentReq</code>,
eliminating per-request listener accumulation,
<code>MaxListenersExceededWarning</code>, and linear heap growth under
concurrent or long-running keep-alive workloads (fixes <a
href="https://redirect.github.com/axios/axios/issues/10780">#10780</a>).
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10788">#10788</a></strong>)</li>
</ul>
<h2>🔧 Maintenance &amp; Chores</h2>
<ul>
<li><strong>Changelog:</strong> Updated <code>CHANGELOG.md</code> with
v1.15.1 release notes. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10781">#10781</a></strong>)</li>
</ul>
<p><a
href="https://github.com/axios/axios/compare/v1.15.1...v1.15.2">Full
Changelog</a></p>
<hr />
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/axios/axios/commit/582934382e4e0e0bcb679c628071a4203e93cf57"><code>5829343</code></a>
chore(release): prepare release 1.15.2 (<a
href="https://redirect.github.com/axios/axios/issues/10789">#10789</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/4709a48fa2717ba97f43f5432d48ca4e26c2d326"><code>4709a48</code></a>
fix: added fix for memory leak in sockets (<a
href="https://redirect.github.com/axios/axios/issues/10788">#10788</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/be3336014e01f9a4fc1f8aef15303cf7daaf58db"><code>be33360</code></a>
chore: update changelog (<a
href="https://redirect.github.com/axios/axios/issues/10781">#10781</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/47915144662f2733e6c051bdcb895a8c8f0586aa"><code>4791514</code></a>
fix: more header pollutions (<a
href="https://redirect.github.com/axios/axios/issues/10779">#10779</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/6feafcff6c2dbafe206161c5d09e38e1d36af66f"><code>6feafcf</code></a>
fix: socket issue (<a
href="https://redirect.github.com/axios/axios/issues/10777">#10777</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/302e2739c602f00e323d4f3f5c79500647633a73"><code>302e273</code></a>
docs: update docs, add a couple actions etc (<a
href="https://redirect.github.com/axios/axios/issues/10776">#10776</a>)</li>
<li>See full diff in <a
href="https://github.com/axios/axios/compare/v1.15.1...v1.15.2">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-03 13:56:59 +02:00
Martin f766c3b789 various dependency upgrades (#201) 2026-05-03 12:06:50 +02:00
Martin 8305746754 add license info for icons (#200)
solves https://github.com/anuraghazra/github-readme-stats/issues/4839
2026-05-03 11:20:05 +02:00
Martin b863992994 upgrade transitive dependencies and pnpm (#190) 2026-04-27 10:43:51 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fe5ef06049 build(deps-dev): Bump knip from 6.4.1 to 6.5.0 (#188)
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip)
from 6.4.1 to 6.5.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/webpro-nl/knip/releases">knip's
releases</a>.</em></p>
<blockquote>
<h2>Release 6.5.0</h2>
<ul>
<li>Drop project-extension-redundant config hint (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1683">#1683</a>)
(f86092949d6dbd041fd621876da674fd3eac7534)</li>
<li>Add instructions to .agents/PLUGINS.md
(e2943ed8fb6d2c0ab4cf12ff04d10bd5ab9fd5f7)</li>
<li>Add a plugin for react-email (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1684">#1684</a>)
(d8ae4d3ccb810b9a9211fd43e9d1e7d7b704fcaf) - thanks <a
href="https://github.com/xaqrox"><code>@​xaqrox</code></a>!</li>
<li>Replace <code>fast-glob</code> with <code>tinyglobby</code> (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1462">#1462</a>)
(9f6b4c8aa6857fea40e66d008c905c948af10939) - thanks <a
href="https://github.com/gameroman"><code>@​gameroman</code></a>!</li>
<li>Added plugin for Serverless framework (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1478">#1478</a>)
(f885f1ded52427d984c67e7172e3141eb4e5ee29) - thanks <a
href="https://github.com/BenCrinion-IW"><code>@​BenCrinion-IW</code></a>!</li>
<li>Add args to Prettier plugin to resolve --config CLI flag (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1685">#1685</a>)
(f4658c84e714afd8a2233b72be6169da1ac3723e) - thanks <a
href="https://github.com/xaqrox"><code>@​xaqrox</code></a>!</li>
<li>fix(util): tolerate JSONC and array extends in findRootDirsBase (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1681">#1681</a>)
(f7e5464a6f70e9024a341fdb923766b5ff53a831) - thanks <a
href="https://github.com/Hoffs"><code>@​Hoffs</code></a>!</li>
<li>Format (15bd7e7a56b470096cfee1690263d5d13ddb8fb6)</li>
<li>Inherit outDir/rootDir from tsconfig project references (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1680">#1680</a>)
(b89b4f716f4c117b0106e9a212e9e5c46aa85035)</li>
<li>tsc → tsgo (a6e09ca1b65936b3790a6c3628a4646f030d18b8)</li>
<li>Update dependencies (4cb05c96a2ae7c790d29ee76ff13288c5bbb97a2)</li>
<li>Housekeep (28c56cb8bf68031b1c8e9bee75b18ee7274ec981)</li>
<li>Test test test (7eb4ab3a8a6635dcaf756bb2b4a88e1298615994)</li>
<li>Add pino plugin with <code>transportCall</code> visitor (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1480">#1480</a>)
(53a033e4ddc5036c6a4a0e55c0abc42c5c64e4f5)</li>
<li>Add signal to projects using knip
(dbedd665c1f8d735030600d3f68ef1825d9a2668)</li>
<li>Tune logos (3148f4d0485875370634b9b53c1b3aa7f6eafcc3)</li>
<li>Tweak <code>knip-run</code> tool response
(42940381a947c46996ae4055e8789f6ea39cfca4)</li>
<li>Add <code>workspace</code> option to <code>knip-run</code> tool
(64c4aaea89e3abb41d8695ebffc5538878520b21)</li>
<li>Remove old lingering <code>experimentalTags</code>
(e503d108e5535800ac6467f4d92c7dd6a9e90037)</li>
<li>Fix compiler type (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1689">#1689</a>)
(e7a69adb5e584eb6e5af9b4007820afcbcf27a08)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/webpro-nl/knip/commit/5b5cda5b22e7989c9aa78746257ca8b1f4504f94"><code>5b5cda5</code></a>
Release knip@6.5.0</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/e7a69adb5e584eb6e5af9b4007820afcbcf27a08"><code>e7a69ad</code></a>
Fix compiler type (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1689">#1689</a>)</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/e503d108e5535800ac6467f4d92c7dd6a9e90037"><code>e503d10</code></a>
Remove old lingering <code>experimentalTags</code></li>
<li><a
href="https://github.com/webpro-nl/knip/commit/53a033e4ddc5036c6a4a0e55c0abc42c5c64e4f5"><code>53a033e</code></a>
Add pino plugin with <code>transportCall</code> visitor (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1480">#1480</a>)</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/7eb4ab3a8a6635dcaf756bb2b4a88e1298615994"><code>7eb4ab3</code></a>
Test test test</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/28c56cb8bf68031b1c8e9bee75b18ee7274ec981"><code>28c56cb</code></a>
Housekeep</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/4cb05c96a2ae7c790d29ee76ff13288c5bbb97a2"><code>4cb05c9</code></a>
Update dependencies</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/a6e09ca1b65936b3790a6c3628a4646f030d18b8"><code>a6e09ca</code></a>
tsc → tsgo</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/b89b4f716f4c117b0106e9a212e9e5c46aa85035"><code>b89b4f7</code></a>
Inherit outDir/rootDir from tsconfig project references (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1680">#1680</a>)</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/15bd7e7a56b470096cfee1690263d5d13ddb8fb6"><code>15bd7e7</code></a>
Format</li>
<li>Additional commits viewable in <a
href="https://github.com/webpro-nl/knip/commits/knip@6.5.0/packages/knip">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 10:18:22 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 134b738151 build(deps-dev): Bump the eslint group across 1 directory with 2 updates (#183)
Bumps the eslint group with 2 updates in the / directory:
[eslint](https://github.com/eslint/eslint) and
[eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks).

Updates `eslint` from 10.2.0 to 10.2.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/eslint/eslint/releases">eslint's
releases</a>.</em></p>
<blockquote>
<h2>v10.2.1</h2>
<h2>Bug Fixes</h2>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/14be92b6d1fa0923b8923830f2208e5e2705b002"><code>14be92b</code></a>
fix: model generator yield resumption paths in code path analysis (<a
href="https://redirect.github.com/eslint/eslint/issues/20665">#20665</a>)
(sethamus)</li>
<li><a
href="https://github.com/eslint/eslint/commit/84a19d2c32255db6b9cfc08644a607aae6d5cb62"><code>84a19d2</code></a>
fix: no-async-promise-executor false positives for shadowed Promise (<a
href="https://redirect.github.com/eslint/eslint/issues/20740">#20740</a>)
(xbinaryx)</li>
<li><a
href="https://github.com/eslint/eslint/commit/af764af0ec38225755fbf8a6f207f0c77b595a8d"><code>af764af</code></a>
fix: clarify language and processor validation errors (<a
href="https://redirect.github.com/eslint/eslint/issues/20729">#20729</a>)
(Pixel998)</li>
<li><a
href="https://github.com/eslint/eslint/commit/e251b89a38280973e468a4a9386c138f4f55d10d"><code>e251b89</code></a>
fix: update eslint (<a
href="https://redirect.github.com/eslint/eslint/issues/20715">#20715</a>)
(renovate[bot])</li>
</ul>
<h2>Documentation</h2>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/ca92ca0fb4599e8de1e2fb914e695fe7397cbe63"><code>ca92ca0</code></a>
docs: reuse markdown-it instance for markdown filter (<a
href="https://redirect.github.com/eslint/eslint/issues/20768">#20768</a>)
(Amaresh S M)</li>
<li><a
href="https://github.com/eslint/eslint/commit/57d2ee213305cee0cb55ef08e0480b57396269a9"><code>57d2ee2</code></a>
docs: Enable Eleventy incremental mode for watch (<a
href="https://redirect.github.com/eslint/eslint/issues/20767">#20767</a>)
(Amaresh S M)</li>
<li><a
href="https://github.com/eslint/eslint/commit/c1621b915742276e5f4b25efe790ca62296330dc"><code>c1621b9</code></a>
docs: fix typos in code-path-analyzer.js (<a
href="https://redirect.github.com/eslint/eslint/issues/20700">#20700</a>)
(Ayush Shukla)</li>
<li><a
href="https://github.com/eslint/eslint/commit/1418d522d10bde1960f4942afb548bc7160ec49e"><code>1418d52</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/39771e6e600f0b0617fdeafff6dd07e4211ffde6"><code>39771e6</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/71e04693def2df57268f08f3072a2749df6bf438"><code>71e0469</code></a>
docs: fix incomplete JSDoc param description in no-shadow rule (<a
href="https://redirect.github.com/eslint/eslint/issues/20728">#20728</a>)
(kuldeep kumar)</li>
<li><a
href="https://github.com/eslint/eslint/commit/22119ceb93e28f62262fc1d98ff1b1442d6e2dbf"><code>22119ce</code></a>
docs: clarify scope of for-direction rule with dead code examples (<a
href="https://redirect.github.com/eslint/eslint/issues/20723">#20723</a>)
(Amaresh S M)</li>
<li><a
href="https://github.com/eslint/eslint/commit/8f3fb77f122a5641d1833cad5d93f3f54fa3be0b"><code>8f3fb77</code></a>
docs: document <code>meta.docs.dialects</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/20718">#20718</a>)
(Pixel998)</li>
</ul>
<h2>Chores</h2>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/7ddfea9c4f62add1588c5c0b0da568c299246383"><code>7ddfea9</code></a>
chore: update dependency prettier to v3.8.2 (<a
href="https://redirect.github.com/eslint/eslint/issues/20770">#20770</a>)
(renovate[bot])</li>
<li><a
href="https://github.com/eslint/eslint/commit/fac40e1de2ba7646cc7cd2d3f93fbdd1f8819001"><code>fac40e1</code></a>
ci: bump pnpm/action-setup from 5.0.0 to 6.0.0 (<a
href="https://redirect.github.com/eslint/eslint/issues/20763">#20763</a>)
(dependabot[bot])</li>
<li><a
href="https://github.com/eslint/eslint/commit/7246f923332522d8b3d46b6ee646fce88535f3fb"><code>7246f92</code></a>
test: add tests for SuppressionsService.load() error handling (<a
href="https://redirect.github.com/eslint/eslint/issues/20734">#20734</a>)
(kuldeep kumar)</li>
<li><a
href="https://github.com/eslint/eslint/commit/4f34b1e592b0f63d766d9903998e8e36eb49d3aa"><code>4f34b1e</code></a>
chore: update pnpm/action-setup action to v5 (<a
href="https://redirect.github.com/eslint/eslint/issues/20762">#20762</a>)
(renovate[bot])</li>
<li><a
href="https://github.com/eslint/eslint/commit/51080eb5c98d619434e4835dbe9f1c6654aca3b8"><code>51080eb</code></a>
test: processor service (<a
href="https://redirect.github.com/eslint/eslint/issues/20731">#20731</a>)
(kuldeep kumar)</li>
<li><a
href="https://github.com/eslint/eslint/commit/e7e1889fca9b6044e08f41b38df20a1ce45808c8"><code>e7e1889</code></a>
chore: remove stale babel-eslint10 fixture and test (<a
href="https://redirect.github.com/eslint/eslint/issues/20727">#20727</a>)
(kuldeep kumar)</li>
<li><a
href="https://github.com/eslint/eslint/commit/4e1a87cb8fb90e309524bc36bc5f31b9f9cfaa76"><code>4e1a87c</code></a>
test: remove redundant async/await in flat config array tests (<a
href="https://redirect.github.com/eslint/eslint/issues/20722">#20722</a>)
(Pixel998)</li>
<li><a
href="https://github.com/eslint/eslint/commit/066eabb3643b12931f991594969bcc0028f71a5f"><code>066eabb</code></a>
test: add rule metadata coverage for <code>languages</code> and
<code>docs.dialects</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/20717">#20717</a>)
(Pixel998)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/4d1d8f9737236603f64bbe83d5bb8001627b5611"><code>4d1d8f9</code></a>
10.2.1</li>
<li><a
href="https://github.com/eslint/eslint/commit/3e33105b05d09b5a4eb894ed75a9811fb40d65e6"><code>3e33105</code></a>
Build: changelog update for 10.2.1</li>
<li><a
href="https://github.com/eslint/eslint/commit/ca92ca0fb4599e8de1e2fb914e695fe7397cbe63"><code>ca92ca0</code></a>
docs: reuse markdown-it instance for markdown filter (<a
href="https://redirect.github.com/eslint/eslint/issues/20768">#20768</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/7ddfea9c4f62add1588c5c0b0da568c299246383"><code>7ddfea9</code></a>
chore: update dependency prettier to v3.8.2 (<a
href="https://redirect.github.com/eslint/eslint/issues/20770">#20770</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/57d2ee213305cee0cb55ef08e0480b57396269a9"><code>57d2ee2</code></a>
docs: Enable Eleventy incremental mode for watch (<a
href="https://redirect.github.com/eslint/eslint/issues/20767">#20767</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/c1621b915742276e5f4b25efe790ca62296330dc"><code>c1621b9</code></a>
docs: fix typos in code-path-analyzer.js (<a
href="https://redirect.github.com/eslint/eslint/issues/20700">#20700</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/fac40e1de2ba7646cc7cd2d3f93fbdd1f8819001"><code>fac40e1</code></a>
ci: bump pnpm/action-setup from 5.0.0 to 6.0.0 (<a
href="https://redirect.github.com/eslint/eslint/issues/20763">#20763</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/7246f923332522d8b3d46b6ee646fce88535f3fb"><code>7246f92</code></a>
test: add tests for SuppressionsService.load() error handling (<a
href="https://redirect.github.com/eslint/eslint/issues/20734">#20734</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/4f34b1e592b0f63d766d9903998e8e36eb49d3aa"><code>4f34b1e</code></a>
chore: update pnpm/action-setup action to v5 (<a
href="https://redirect.github.com/eslint/eslint/issues/20762">#20762</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/1418d522d10bde1960f4942afb548bc7160ec49e"><code>1418d52</code></a>
docs: Update README</li>
<li>Additional commits viewable in <a
href="https://github.com/eslint/eslint/compare/v10.2.0...v10.2.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `eslint-plugin-react-hooks` from 7.1.0 to 7.1.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/facebook/react/releases">eslint-plugin-react-hooks's
releases</a>.</em></p>
<blockquote>
<h2>eslint-plugin-react-hooks@7.1.1 (April 17, 2026)</h2>
<p><strong>Note:</strong> 7.1.0 accidentally removed the
<code>component-hook-factories</code> rule, causing errors for users who
referenced it in their ESLint config. This is now fixed.</p>
<ul>
<li>Add deprecated no-op <code>component-hook-factories</code> rule for
backwards compatibility. (<a
href="https://github.com/mofeiZ"><code>@​mofeiZ</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/36307">#36307</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md">eslint-plugin-react-hooks's
changelog</a>.</em></p>
<blockquote>
<h2>7.1.1</h2>
<p><strong>Note:</strong> 7.1.0 accidentally removed the
<code>component-hook-factories</code> rule, causing errors for users who
referenced it in their ESLint config. This is now fixed.</p>
<ul>
<li>Add deprecated no-op <code>component-hook-factories</code> rule for
backwards compatibility. (<a
href="https://github.com/mofeiZ"><code>@​mofeiZ</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/36307">#36307</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/facebook/react/commit/d1727fbf987392749b374f3afe62227d2c70fc41"><code>d1727fb</code></a>
[eprh] Update changelog for 7.1.1 (<a
href="https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks/issues/36308">#36308</a>)</li>
<li><a
href="https://github.com/facebook/react/commit/bc249804d3c2ea6ee95bc5543c3735a65d1239b5"><code>bc24980</code></a>
[eprh] Add back a no-op for removed component-hook-factories rule (<a
href="https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks/issues/36307">#36307</a>)</li>
<li>See full diff in <a
href="https://github.com/facebook/react/commits/eslint-plugin-react-hooks@7.1.1/packages/eslint-plugin-react-hooks">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 10:15:11 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3263dd9c78 build(deps): Bump react-toastify from 11.0.5 to 11.1.0 in the react group across 1 directory (#182)
Bumps the react group with 1 update in the / directory:
[react-toastify](https://github.com/fkhadra/react-toastify).

Updates `react-toastify` from 11.0.5 to 11.1.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/fkhadra/react-toastify/releases">react-toastify's
releases</a>.</em></p>
<blockquote>
<h2>v11.1.0</h2>
<h1>Release Notes</h1>
<h2>Features</h2>
<ul>
<li><strong>CSP nonce support.</strong> <code>&lt;ToastContainer
nonce={...}&gt;</code> applies the nonce to the
injected <code>&lt;style&gt;</code> tag. Closes <a
href="https://redirect.github.com/fkhadra/react-toastify/issues/1209">#1209</a>.</li>
</ul>
<h2>Fixes</h2>
<ul>
<li><code>onChange</code> fires <code>status: 'removed'</code>
synchronously on <code>toast.dismiss()</code> instead
of after the exit animation — observers (incl.
<code>useNotificationCenter</code>) now
see correctly ordered events. Also guards against
double-<code>onClose</code>. Closes <a
href="https://redirect.github.com/fkhadra/react-toastify/issues/1275">#1275</a>.</li>
<li>Touch drag no longer re-pauses the toast on release — the old check
compared a
PointerEvent against <code>'touchend'</code>, which never matched.
Closes <a
href="https://redirect.github.com/fkhadra/react-toastify/issues/1217">#1217</a>.</li>
<li>Vertical drag now visually moves the toast (<code>--y</code> gets a
unit). Thanks
<a href="https://github.com/janpaepke"><code>@​janpaepke</code></a>, <a
href="https://redirect.github.com/fkhadra/react-toastify/issues/1277">#1277</a>.</li>
<li>Stacked scale is clamped at 0.5, preventing zero/negative scale in
deep
stacks. Closes <a
href="https://redirect.github.com/fkhadra/react-toastify/issues/1171">#1171</a>,
<a
href="https://redirect.github.com/fkhadra/react-toastify/issues/1174">#1174</a>.</li>
<li>Stacked container respects mobile <code>100vw</code> again. Closes
<a
href="https://redirect.github.com/fkhadra/react-toastify/issues/1234">#1234</a>.</li>
</ul>
<h2>Accessibility</h2>
<ul>
<li><code>role=&quot;progressbar&quot;</code> now includes
<code>aria-valuenow</code>, <code>aria-valuemin</code>,
<code>aria-valuemax</code>.
Thanks <a
href="https://github.com/singhankit001"><code>@​singhankit001</code></a>,
<a
href="https://redirect.github.com/fkhadra/react-toastify/issues/1283">#1283</a>.
Closes <a
href="https://redirect.github.com/fkhadra/react-toastify/issues/1259">#1259</a>.</li>
</ul>
<h2>Internal</h2>
<ul>
<li>Migrated to a pnpm workspace (<code>pnpm link .</code> no longer
required for contributors).
Publish layout unchanged — addon still ships inside the main
package.</li>
<li>CSS now injected at mount via <code>useStyleSheet</code>
(prerequisite for <code>nonce</code>).</li>
<li>Dep bumps: TypeScript 6, Vite 8, Cypress 15, React 19.2, plus the
rest.</li>
<li>CI: <code>upload-artifact</code> v3 → v4.</li>
</ul>
<p>Thanks to <a
href="https://github.com/janpaepke"><code>@​janpaepke</code></a>, <a
href="https://github.com/singhankit001"><code>@​singhankit001</code></a>,
and reporters of the fixed issues.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/fkhadra/react-toastify/commit/769ce82f1c72cde5ee681068194adfa88a0c81c9"><code>769ce82</code></a>
11.1.0</li>
<li><a
href="https://github.com/fkhadra/react-toastify/commit/de219584ade15c4973c4eedf6ef68af88f64952f"><code>de21958</code></a>
remove year from license</li>
<li><a
href="https://github.com/fkhadra/react-toastify/commit/c800992ed72a47351f4d3d54a5be7fbca68765d2"><code>c800992</code></a>
fix: add unit to --y CSS variable to fix vertical drag</li>
<li><a
href="https://github.com/fkhadra/react-toastify/commit/e4ab71249e1afa5f975f9be12f8542c88f02efe9"><code>e4ab712</code></a>
fix chicken and egg issue</li>
<li><a
href="https://github.com/fkhadra/react-toastify/commit/b3bbab5468777ea945d70a82b068cb6d105d4fe7"><code>b3bbab5</code></a>
bump artifact action</li>
<li><a
href="https://github.com/fkhadra/react-toastify/commit/6d58ce465e9083a7f695a4df669d5cbc548a97af"><code>6d58ce4</code></a>
fix: stacked toast invert size, when used as stacked</li>
<li><a
href="https://github.com/fkhadra/react-toastify/commit/50e8e420e1e87fd75cfb7058728bccde46cfa790"><code>50e8e42</code></a>
fix: stacked container on mobile not 100vw</li>
<li><a
href="https://github.com/fkhadra/react-toastify/commit/1b2ee1762524893a5f3d66f5a0ed3c75d013a731"><code>1b2ee17</code></a>
fix: dispatch 'removed' onChange event synchronously on dismiss</li>
<li><a
href="https://github.com/fkhadra/react-toastify/commit/a2d376b408c23c42ead74460ecd941a2ca0c9e9f"><code>a2d376b</code></a>
feat: add ARIA accessibility attributes to ProgressBar</li>
<li><a
href="https://github.com/fkhadra/react-toastify/commit/8375aced2da12884f1c9b01bd79e20906ea5bd08"><code>8375ace</code></a>
fix: touch bug in onDragTransitionEnd, closes <a
href="https://redirect.github.com/fkhadra/react-toastify/issues/1217">#1217</a></li>
<li>Additional commits viewable in <a
href="https://github.com/fkhadra/react-toastify/compare/v11.0.5...v11.1.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 10:14:20 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b4ae648153 build(deps): Bump axios from 1.15.0 to 1.15.1 in the axios group across 1 directory (#184)
Bumps the axios group with 1 update in the / directory:
[axios](https://github.com/axios/axios).

Updates `axios` from 1.15.0 to 1.15.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/releases">axios's
releases</a>.</em></p>
<blockquote>
<h2>v1.15.1</h2>
<p>This release ships a coordinated set of security hardening fixes
across headers, body/redirect limits, multipart handling, and
XSRF/prototype-pollution vectors, alongside a broad sweep of bug fixes,
test migrations, and threat-model documentation updates.</p>
<h2>🔒 Security Fixes</h2>
<ul>
<li><strong>Header Injection Hardening:</strong> Tightened validation
and sanitisation across request header construction to close the
header-injection attack surface. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10749">#10749</a></strong>)</li>
<li><strong>CRLF Stripping in Multipart Headers:</strong> Correctly
strips CR/LF from multipart header values to prevent injection via field
names and filenames. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10758">#10758</a></strong>)</li>
<li><strong>Prototype Pollution / Auth Bypass:</strong> Replaced unsafe
<code>in</code> checks with <code>hasOwnProperty</code> to prevent
authentication bypass via prototype pollution on config objects, with
additional regression tests. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10761">#10761</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10760">#10760</a></strong>)</li>
<li><strong><code>withXSRFToken</code> Truthy Bypass:</strong>
Short-circuits on any truthy non-boolean value, so an ambiguous config
no longer silently leaks the XSRF token cross-origin. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10762">#10762</a></strong>)</li>
<li><strong><code>maxBodyLength</code> With Zero Redirects:</strong>
Enforces <code>maxBodyLength</code> even when <code>maxRedirects</code>
is set to <code>0</code>, closing a bypass path for oversized request
bodies. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10753">#10753</a></strong>)</li>
<li><strong>Streamed Response <code>maxContentLength</code>
Bypass:</strong> Applies <code>maxContentLength</code> to streamed
responses that previously bypassed the cap. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10754">#10754</a></strong>)</li>
<li><strong>Follow-up CVE Completion:</strong> Completes an earlier
incomplete CVE fix to fully close the regression window. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10755">#10755</a></strong>)</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li><strong>AI-Based Docs Translations:</strong> Initial scaffold for
AI-assisted translations of the documentation site. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10705">#10705</a></strong>)</li>
<li><strong><code>Location</code> Request Header Type:</strong> Adds
<code>Location</code> to <code>CommonRequestHeadersList</code> for
accurate typing of redirect-aware requests. (<strong><a
href="https://redirect.github.com/axios/axios/issues/7528">#7528</a></strong>)</li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li><strong>FormData Handling:</strong> Removes
<code>Content-Type</code> when no boundary is present on
<code>FormData</code> fetch requests, supports multi-select fields,
cancels <code>request.body</code> instead of the source stream on fetch
abort, and fixes a recursion bug in form-data serialisation. (<strong><a
href="https://redirect.github.com/axios/axios/issues/7314">#7314</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10676">#10676</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10702">#10702</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10726">#10726</a></strong>)</li>
<li><strong>HTTP Adapter:</strong> Handles socket-only request errors
without leaking keep-alive listeners. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10576">#10576</a></strong>)</li>
<li><strong>Progress Events:</strong> Clamps <code>loaded</code> to
<code>total</code> for computable upload/download progress events.
(<strong><a
href="https://redirect.github.com/axios/axios/issues/7458">#7458</a></strong>)</li>
<li><strong>Types:</strong> Aligns <code>runWhen</code> type with the
runtime behaviour in <code>InterceptorManager</code> and makes response
header keys case-insensitive. (<strong><a
href="https://redirect.github.com/axios/axios/issues/7529">#7529</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10677">#10677</a></strong>)</li>
<li><strong><code>buildFullPath</code>:</strong> Uses strict equality in
the base/relative URL check. (<strong><a
href="https://redirect.github.com/axios/axios/issues/7252">#7252</a></strong>)</li>
<li><strong><code>AxiosURLSearchParams</code> Regex:</strong> Improves
the regex used for param serialisation to avoid edge-case mismatches.
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10736">#10736</a></strong>)</li>
<li><strong>Resilient Value Parsing:</strong> Parses out header/config
values instead of throwing on malformed input. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10687">#10687</a></strong>)</li>
<li><strong>Docs Artefact Cleanup:</strong> Removes the docs content
that was incorrectly committed. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10727">#10727</a></strong>)</li>
</ul>
<h2>🔧 Maintenance &amp; Chores</h2>
<ul>
<li><strong>Threat Model &amp; Security Docs:</strong> Ongoing
refinement of <code>THREATMODEL.md</code>, including Hopper security
update, TLS and tag-replay wording, mitigation descriptions,
decompression-bomb guidance, and further cleanup. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10672">#10672</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10715">#10715</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10718">#10718</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10722">#10722</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10763">#10763</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10765">#10765</a></strong>)</li>
<li><strong>Test Coverage &amp; Migration:</strong> Expanded
<code>shouldBypassProxy</code> coverage for wildcard/IPv6/edge cases,
documented and tested <code>AxiosError.status</code>, and migrated
<code>progressEventReducer</code> tests to Vitest. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10723">#10723</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10725">#10725</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10741">#10741</a></strong>)</li>
<li><strong>Type Refactor:</strong> Uses TypeScript utility types to
deduplicate literal unions. (<strong><a
href="https://redirect.github.com/axios/axios/issues/7520">#7520</a></strong>)</li>
<li><strong>Repo &amp; CI:</strong> Adds <code>CODEOWNERS</code>,
switches v1.x releases to an ephemeral release branch, and removes
orphaned Bower support. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10739">#10739</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10738">#10738</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10746">#10746</a></strong>)</li>
<li><strong>Changelog Backfill:</strong> Added missing version entries
to the changelog. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10704">#10704</a></strong>)</li>
<li><strong>Dependencies:</strong> Bumped <code>follow-redirects</code>
(<code>1.15.11</code> → <code>1.16.0</code>) in root and docs,
<code>axios</code> (<code>1.14.0</code> → <code>1.15.0</code>) in docs,
and a group of 5 development dependencies. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10717">#10717</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10716">#10716</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10684">#10684</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10709">#10709</a></strong>)</li>
</ul>
<h2>🌟 New Contributors</h2>
<p>We are thrilled to welcome our new contributors. Thank you for
helping improve axios:</p>
<ul>
<li><strong><a
href="https://github.com/curiouscoder-cmd"><code>@​curiouscoder-cmd</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/7252">#7252</a></strong>)</li>
<li><strong><a
href="https://github.com/tryonelove"><code>@​tryonelove</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/7520">#7520</a></strong>)</li>
<li><strong><a
href="https://github.com/darwin808"><code>@​darwin808</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/7314">#7314</a></strong>)</li>
<li><strong><a
href="https://github.com/zoontek"><code>@​zoontek</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10702">#10702</a></strong>)</li>
<li><strong><a
href="https://github.com/AKIB473"><code>@​AKIB473</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10725">#10725</a></strong>)</li>
</ul>
<p><a
href="https://github.com/axios/axios/compare/v1.15.0...v1.15.1">Full
Changelog</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/blob/v1.x/CHANGELOG.md">axios's
changelog</a>.</em></p>
<blockquote>
<h2>v1.15.1 - April 19, 2026</h2>
<p>This release ships a coordinated set of security hardening fixes
across headers, body/redirect limits, multipart handling, and
XSRF/prototype-pollution vectors, alongside a broad sweep of bug fixes,
test migrations, and threat-model documentation updates.</p>
<h2>🔒 Security Fixes</h2>
<ul>
<li>
<p><strong>Header Injection Hardening:</strong> Tightened validation and
sanitisation across request header construction to close the
header-injection attack surface. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10749">#10749</a></strong>)</p>
</li>
<li>
<p><strong>CRLF Stripping in Multipart Headers:</strong> Correctly
strips CR/LF from multipart header values to prevent injection via field
names and filenames. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10758">#10758</a></strong>)</p>
</li>
<li>
<p><strong>Prototype Pollution / Auth Bypass:</strong> Replaced unsafe
<code>in</code> checks with <code>hasOwnProperty</code> to prevent
authentication bypass via prototype pollution on config objects, with
additional regression tests. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10761">#10761</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10760">#10760</a></strong>)</p>
</li>
<li>
<p><strong><code>withXSRFToken</code> Truthy Bypass:</strong>
Short-circuits on any truthy non-boolean value, so an ambiguous config
no longer silently leaks the XSRF token cross-origin. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10762">#10762</a></strong>)</p>
</li>
<li>
<p><strong><code>maxBodyLength</code> With Zero Redirects:</strong>
Enforces <code>maxBodyLength</code> even when <code>maxRedirects</code>
is set to <code>0</code>, closing a bypass path for oversized request
bodies. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10753">#10753</a></strong>)</p>
</li>
<li>
<p><strong>Streamed Response <code>maxContentLength</code>
Bypass:</strong> Applies <code>maxContentLength</code> to streamed
responses that previously bypassed the cap. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10754">#10754</a></strong>)</p>
</li>
<li>
<p><strong>Follow-up CVE Completion:</strong> Completes an earlier
incomplete CVE fix to fully close the regression window. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10755">#10755</a></strong>)</p>
</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li>
<p><strong>AI-Based Docs Translations:</strong> Initial scaffold for
AI-assisted translations of the documentation site. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10705">#10705</a></strong>)</p>
</li>
<li>
<p><strong><code>Location</code> Request Header Type:</strong> Adds
<code>Location</code> to <code>CommonRequestHeadersList</code> for
accurate typing of redirect-aware requests. (<strong><a
href="https://redirect.github.com/axios/axios/issues/7528">#7528</a></strong>)</p>
</li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li>
<p><strong>FormData Handling:</strong> Removes <code>Content-Type</code>
when no boundary is present on <code>FormData</code> fetch requests,
supports multi-select fields, cancels <code>request.body</code> instead
of the source stream on fetch abort, and fixes a recursion bug in
form-data serialisation. (<strong><a
href="https://redirect.github.com/axios/axios/issues/7314">#7314</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10676">#10676</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10702">#10702</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10726">#10726</a></strong>)</p>
</li>
<li>
<p><strong>HTTP Adapter:</strong> Handles socket-only request errors
without leaking keep-alive listeners. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10576">#10576</a></strong>)</p>
</li>
<li>
<p><strong>Progress Events:</strong> Clamps <code>loaded</code> to
<code>total</code> for computable upload/download progress events.
(<strong><a
href="https://redirect.github.com/axios/axios/issues/7458">#7458</a></strong>)</p>
</li>
<li>
<p><strong>Types:</strong> Aligns <code>runWhen</code> type with the
runtime behaviour in <code>InterceptorManager</code> and makes response
header keys case-insensitive. (<strong><a
href="https://redirect.github.com/axios/axios/issues/7529">#7529</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10677">#10677</a></strong>)</p>
</li>
<li>
<p><strong><code>buildFullPath</code>:</strong> Uses strict equality in
the base/relative URL check. (<strong><a
href="https://redirect.github.com/axios/axios/issues/7252">#7252</a></strong>)</p>
</li>
<li>
<p><strong><code>AxiosURLSearchParams</code> Regex:</strong> Improves
the regex used for param serialisation to avoid edge-case mismatches.
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10736">#10736</a></strong>)</p>
</li>
<li>
<p><strong>Resilient Value Parsing:</strong> Parses out header/config
values instead of throwing on malformed input. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10687">#10687</a></strong>)</p>
</li>
<li>
<p><strong>Docs Artefact Cleanup:</strong> Removes the docs content that
was incorrectly committed. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10727">#10727</a></strong>)</p>
</li>
</ul>
<h2>🔧 Maintenance &amp; Chores</h2>
<ul>
<li>
<p><strong>Threat Model &amp; Security Docs:</strong> Ongoing refinement
of <code>THREATMODEL.md</code>, including Hopper security update, TLS
and tag-replay wording, mitigation descriptions, decompression-bomb
guidance, and further cleanup. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10672">#10672</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10715">#10715</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10718">#10718</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10722">#10722</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10763">#10763</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10765">#10765</a></strong>)</p>
</li>
<li>
<p><strong>Test Coverage &amp; Migration:</strong> Expanded
<code>shouldBypassProxy</code> coverage for wildcard/IPv6/edge cases,
documented and tested <code>AxiosError.status</code>, and migrated
<code>progressEventReducer</code> tests to Vitest. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10723">#10723</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10725">#10725</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10741">#10741</a></strong>)</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/axios/axios/commit/ac42446be51300fe214ba3c6e40cc95f34fd6871"><code>ac42446</code></a>
chore(release): prepare release 1.15.1 (<a
href="https://redirect.github.com/axios/axios/issues/10767">#10767</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/908f2206b6bfeff67236784abce85935698ac1d9"><code>908f220</code></a>
docs: update threatmodel (<a
href="https://redirect.github.com/axios/axios/issues/10765">#10765</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/f93f8155250c2e066205521eda05ae22983a1f6d"><code>f93f815</code></a>
docs: added docs around potential decompressions bomb (<a
href="https://redirect.github.com/axios/axios/issues/10763">#10763</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/1728aa1b15b8857f970611fd8983c06b423fc486"><code>1728aa1</code></a>
fix: short-circuits on any truthy non-boolean in withXSRFToken (<a
href="https://redirect.github.com/axios/axios/issues/10762">#10762</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/42eb721eebc95cfded8d6c70cf62f3fbe83f3246"><code>42eb721</code></a>
fix: replace in with has own prop util (<a
href="https://redirect.github.com/axios/axios/issues/10761">#10761</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/75873270a59bd5d895322eee145738e95fb89258"><code>7587327</code></a>
fix: strip crlf correctly (<a
href="https://redirect.github.com/axios/axios/issues/10758">#10758</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/f0b98673b32677643a3b608431270d36e997473c"><code>f0b9867</code></a>
chore: added additional testing for this issue (<a
href="https://redirect.github.com/axios/axios/issues/10760">#10760</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/e033f243a08e3514c03e510f76658da1e0fac3bd"><code>e033f24</code></a>
fix: incomplete fix for cve (<a
href="https://redirect.github.com/axios/axios/issues/10755">#10755</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/e8904af03385b040e53f1263a444e825db4335d9"><code>e8904af</code></a>
fix: stream response bypassed max content length (<a
href="https://redirect.github.com/axios/axios/issues/10754">#10754</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/1c7f6d76e58359daddf38a6e636dd801ffd9c61b"><code>1c7f6d7</code></a>
fix: enforce max body length when max redirects is 0 (<a
href="https://redirect.github.com/axios/axios/issues/10753">#10753</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/axios/axios/compare/v1.15.0...v1.15.1">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 10:13:33 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c401be4d6c build(deps): Bump vite from 8.0.8 to 8.0.9 in the vite group (#187)
Bumps the vite group with 1 update:
[vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).

Updates `vite` from 8.0.8 to 8.0.9
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.9</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.9/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.8...v8.0.9">8.0.9</a>
(2026-04-20)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li>update rolldown to 1.0.0-rc.16 (<a
href="https://redirect.github.com/vitejs/vite/issues/22248">#22248</a>)
(<a
href="https://github.com/vitejs/vite/commit/2947edd57ceb64a0b4dc43269743e8e44e68c09b">2947edd</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>allow binding when strictPort is set but wildcard port is in use (<a
href="https://redirect.github.com/vitejs/vite/issues/22150">#22150</a>)
(<a
href="https://github.com/vitejs/vite/commit/dfc8aa5057dd8ec2b1223980d1e2eeb946ac3384">dfc8aa5</a>)</li>
<li><strong>build:</strong> emptyOutDir should happen for watch rebuilds
(<a
href="https://redirect.github.com/vitejs/vite/issues/22207">#22207</a>)
(<a
href="https://github.com/vitejs/vite/commit/ee522672bb374c7ff95a347f14732491121b1cd6">ee52267</a>)</li>
<li><strong>bundled-dev:</strong> reject requests to HMR patch files in
non potentially trustworthy origins (<a
href="https://redirect.github.com/vitejs/vite/issues/22269">#22269</a>)
(<a
href="https://github.com/vitejs/vite/commit/868f1411a6f474baa4417f2d6524692dd452f760">868f141</a>)</li>
<li><strong>css:</strong> use unique key for cssEntriesMap to prevent
same-basename collision (<a
href="https://redirect.github.com/vitejs/vite/issues/22039">#22039</a>)
(<a
href="https://github.com/vitejs/vite/commit/374bb5d597fcd0485e929565c698d8ed219136f8">374bb5d</a>)</li>
<li><strong>deps:</strong> update all non-major dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22219">#22219</a>)
(<a
href="https://github.com/vitejs/vite/commit/4cd0d6760edd5fb0841abe86538de3c225e880a1">4cd0d67</a>)</li>
<li><strong>deps:</strong> update all non-major dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22268">#22268</a>)
(<a
href="https://github.com/vitejs/vite/commit/c28e9c12a849f80e6fdc93f42283ad2863ab9dbc">c28e9c1</a>)</li>
<li>detect Deno workspace root (fix <a
href="https://redirect.github.com/vitejs/vite/issues/22237">#22237</a>)
(<a
href="https://redirect.github.com/vitejs/vite/issues/22238">#22238</a>)
(<a
href="https://github.com/vitejs/vite/commit/1b793c0e1726467fffd06ffad9bc81c61a840188">1b793c0</a>)</li>
<li><strong>dev:</strong> handle errors in <code>watchChange</code> hook
(<a
href="https://redirect.github.com/vitejs/vite/issues/22188">#22188</a>)
(<a
href="https://github.com/vitejs/vite/commit/fc08bdab9bba871b03689f2f6997c3a4ba4351da">fc08bda</a>)</li>
<li><strong>optimizer:</strong> handle more chars that will be sanitized
(<a
href="https://redirect.github.com/vitejs/vite/issues/22208">#22208</a>)
(<a
href="https://github.com/vitejs/vite/commit/3f24533ac4845ed22547279d1721bd82a35345e3">3f24533</a>)</li>
<li>skip fallback sourcemap generation for <code>?raw</code> imports (<a
href="https://redirect.github.com/vitejs/vite/issues/22148">#22148</a>)
(<a
href="https://github.com/vitejs/vite/commit/3ec9cdaac7936ca32d0956c4cb1eb6e172945996">3ec9cda</a>)</li>
</ul>
<h3>Documentation</h3>
<ul>
<li>align the descriptions in READMEs (<a
href="https://redirect.github.com/vitejs/vite/issues/22231">#22231</a>)
(<a
href="https://github.com/vitejs/vite/commit/44c42b97639bb6ad777e66d752b2829cccb9a27a">44c42b9</a>)</li>
<li>fix reuses wording in dev environment comment (<a
href="https://redirect.github.com/vitejs/vite/issues/22173">#22173</a>)
(<a
href="https://github.com/vitejs/vite/commit/9163412fdfec7fb1656529713326a5b5c5e986ea">9163412</a>)</li>
<li>fix wording in sass error comment (<a
href="https://redirect.github.com/vitejs/vite/issues/22214">#22214</a>)
(<a
href="https://github.com/vitejs/vite/commit/bc5c6a7a498845dff20dc410c395355b79a4b753">bc5c6a7</a>)</li>
<li>update build CLI defaults (<a
href="https://redirect.github.com/vitejs/vite/issues/22261">#22261</a>)
(<a
href="https://github.com/vitejs/vite/commit/605bb97994678a1bb70a8de9a85c29d5f5d48c5a">605bb97</a>)</li>
</ul>
<h3>Miscellaneous Chores</h3>
<ul>
<li><strong>deps:</strong> update dependency dotenv-expand to v13 (<a
href="https://redirect.github.com/vitejs/vite/issues/22271">#22271</a>)
(<a
href="https://github.com/vitejs/vite/commit/0a3887da18812cacb254c616e4dd35631e776fda">0a3887d</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vitejs/vite/commit/ce729f5fa1a5adca373b2adcb0e1b18099164a14"><code>ce729f5</code></a>
release: v8.0.9</li>
<li><a
href="https://github.com/vitejs/vite/commit/605bb97994678a1bb70a8de9a85c29d5f5d48c5a"><code>605bb97</code></a>
docs: update build CLI defaults (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22261">#22261</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/c28e9c12a849f80e6fdc93f42283ad2863ab9dbc"><code>c28e9c1</code></a>
fix(deps): update all non-major dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22268">#22268</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/0a3887da18812cacb254c616e4dd35631e776fda"><code>0a3887d</code></a>
chore(deps): update dependency dotenv-expand to v13 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22271">#22271</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/868f1411a6f474baa4417f2d6524692dd452f760"><code>868f141</code></a>
fix(bundled-dev): reject requests to HMR patch files in non potentially
trust...</li>
<li><a
href="https://github.com/vitejs/vite/commit/3ec9cdaac7936ca32d0956c4cb1eb6e172945996"><code>3ec9cda</code></a>
fix: skip fallback sourcemap generation for <code>?raw</code> imports
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22148">#22148</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/3f24533ac4845ed22547279d1721bd82a35345e3"><code>3f24533</code></a>
fix(optimizer): handle more chars that will be sanitized (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22208">#22208</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/1b793c0e1726467fffd06ffad9bc81c61a840188"><code>1b793c0</code></a>
fix: detect Deno workspace root (fix <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22237">#22237</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22238">#22238</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/fc08bdab9bba871b03689f2f6997c3a4ba4351da"><code>fc08bda</code></a>
fix(dev): handle errors in <code>watchChange</code> hook (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22188">#22188</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/374bb5d597fcd0485e929565c698d8ed219136f8"><code>374bb5d</code></a>
fix(css): use unique key for cssEntriesMap to prevent same-basename
collision...</li>
<li>Additional commits viewable in <a
href="https://github.com/vitejs/vite/commits/v8.0.9/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=8.0.8&new-version=8.0.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 10:09:18 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f0c250a954 build(deps): Bump uuid from 13.0.0 to 14.0.0 (#185)
Bumps [uuid](https://github.com/uuidjs/uuid) from 13.0.0 to 14.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/uuidjs/uuid/releases">uuid's
releases</a>.</em></p>
<blockquote>
<h2>v14.0.0</h2>
<h2><a
href="https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0">14.0.0</a>
(2026-04-19)</h2>
<h3>⚠ BREAKING CHANGES</h3>
<ul>
<li>expect <code>crypto</code> to be global everywhere (requires
node@20+) (<a
href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>)</li>
<li>drop node@18 support (<a
href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>drop node@18 support (<a
href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>) (<a
href="https://github.com/uuidjs/uuid/commit/dc4ddb87272ed2843faccd130bcc41d492688bd3">dc4ddb8</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>expect <code>crypto</code> to be global everywhere (requires
node@20+) (<a
href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>) (<a
href="https://github.com/uuidjs/uuid/commit/f2c235f93059325fa43e1106e624b5291bb523c4">f2c235f</a>)</li>
<li>Use GITHUB_TOKEN for release-please and enable npm provenance (<a
href="https://redirect.github.com/uuidjs/uuid/issues/925">#925</a>) (<a
href="https://github.com/uuidjs/uuid/commit/ffa31383e8e4e1f0b4e22e504561272041b8738c">ffa3138</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md">uuid's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0">14.0.0</a>
(2026-04-19)</h2>
<h3>Security</h3>
<ul>
<li>Fixes <a
href="https://github.com/uuidjs/uuid/security/advisories/GHSA-w5hq-g745-h8pq">GHSA-w5hq-g745-h8pq</a>:
<code>v3()</code>, <code>v5()</code>, and <code>v6()</code> did not
validate that writes would remain within the bounds of a caller-supplied
buffer, allowing out-of-bounds writes when an invalid
<code>offset</code> was provided. A <code>RangeError</code> is now
thrown if <code>offset &lt; 0</code> or <code>offset + 16 &gt;
buf.length</code>.</li>
</ul>
<h3>⚠ BREAKING CHANGES</h3>
<ul>
<li><code>crypto</code> is now expected to be globally defined (requires
node@20+) (<a
href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>)</li>
<li>drop node@18 support (<a
href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>)</li>
<li>upgrade minimum supported TypeScript version to 5.4.3, in keeping
with the project's policy of supporting TypeScript versions released
within the last two years</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/uuidjs/uuid/commit/7c1ea087a8149b57380fc8bb7f68c3a215cb6e4b"><code>7c1ea08</code></a>
chore(main): release 14.0.0 (<a
href="https://redirect.github.com/uuidjs/uuid/issues/926">#926</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/3d2c5b0342f0fcb52a5ac681c3d47c13e7444b34"><code>3d2c5b0</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/f2c235f93059325fa43e1106e624b5291bb523c4"><code>f2c235f</code></a>
fix!: expect <code>crypto</code> to be global everywhere (requires
node@20+) (<a
href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/529ef0899f5dd503d2ee90d690585d63d78bc212"><code>529ef08</code></a>
chore: upgrade TypeScript and fixup types (<a
href="https://redirect.github.com/uuidjs/uuid/issues/927">#927</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/086fd7976f11433edf9ac80be876b3ad243fe087"><code>086fd79</code></a>
chore: update dependencies (<a
href="https://redirect.github.com/uuidjs/uuid/issues/933">#933</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/dc4ddb87272ed2843faccd130bcc41d492688bd3"><code>dc4ddb8</code></a>
feat!: drop node@18 support (<a
href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/0f1f9c9c9cedbae5a1d363d5406c5dfbabe81404"><code>0f1f9c9</code></a>
chore: switch to Biome for parsing and linting (<a
href="https://redirect.github.com/uuidjs/uuid/issues/932">#932</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/e2879e64bf125add903c1eff6e0860542c605013"><code>e2879e6</code></a>
chore: use maintained version of npm-run-all (<a
href="https://redirect.github.com/uuidjs/uuid/issues/930">#930</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/ffa31383e8e4e1f0b4e22e504561272041b8738c"><code>ffa3138</code></a>
fix: Use GITHUB_TOKEN for release-please and enable npm provenance (<a
href="https://redirect.github.com/uuidjs/uuid/issues/925">#925</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/0423d49df2dc8efc300c804731d25f4d7e0fccc4"><code>0423d49</code></a>
docs: remove obsolete v1 option notes (<a
href="https://redirect.github.com/uuidjs/uuid/issues/915">#915</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for uuid since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=uuid&package-manager=npm_and_yarn&previous-version=13.0.0&new-version=14.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 10:08:17 +02:00
Martin a963caaf5b small refactoring: use constants in layout logic (#186) 2026-04-27 08:34:56 +02:00
Martin eab14c8932 top-langs: add hide_values option (#181)
based on https://github.com/anuraghazra/github-readme-stats/pull/4850 by
@claw-explorer

This PR adds a new hide_values query parameter for the Top Languages
card that hides the percentage/byte display values while keeping
progress bars and chart visualizations intact.

<img width="300" height="245" alt="top-langs5"
src="https://github.com/user-attachments/assets/6c7486c3-5611-41c0-95ff-0d9183cea320"
/>
<img width="300" height="350" alt="top-langs"
src="https://github.com/user-attachments/assets/a0cc5bed-3f8e-4934-9c30-3e7d5a240865"
/>
<img width="350" height="215" alt="top-langs2"
src="https://github.com/user-attachments/assets/9d9c06cc-aa0d-4e47-9cdd-bce298583742"
/>
<img width="300" height="350" alt="top-langs3"
src="https://github.com/user-attachments/assets/f4a51889-0b8f-4289-a082-e2107208a123"
/>
<img width="300" height="140" alt="top-langs4"
src="https://github.com/user-attachments/assets/d88c2ea2-0422-482b-aade-8cf5799f12cd"
/>
2026-04-25 20:29:08 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 457c457cab build(deps-dev): Bump eslint-plugin-react-hooks from 7.0.1 to 7.1.0 in the eslint group (#178)
Bumps the eslint group with 1 update:
[eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks).

Updates `eslint-plugin-react-hooks` from 7.0.1 to 7.1.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/facebook/react/releases">eslint-plugin-react-hooks's
releases</a>.</em></p>
<blockquote>
<h2>eslint-plugin-react-hooks@7.1.0 (April 16, 2026)</h2>
<p>This release adds ESLint v10 support, improves performance by
skipping compilation for non-React files, and includes compiler lint
improvements including better <code>set-state-in-effect</code>
detection, improved ref validation, and more helpful error
reporting.</p>
<ul>
<li>Add ESLint v10 support. (<a
href="https://github.com/azat-io"><code>@​azat-io</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35720">#35720</a>)</li>
<li>Skip compilation for non-React files to improve performance. (<a
href="https://github.com/josephsavona"><code>@​josephsavona</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35589">#35589</a>)</li>
<li>Fix exhaustive deps bug with Flow type casting. (<a
href="https://github.com/jorge-cab"><code>@​jorge-cab</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35691">#35691</a>)</li>
<li>Fix <code>useEffectEvent</code> checks in component syntax. (<a
href="https://github.com/jbrown215"><code>@​jbrown215</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35041">#35041</a>)</li>
<li>Improved <code>set-state-in-effect</code> validation with fewer
false negatives. (<a
href="https://github.com/jorge-cab"><code>@​jorge-cab</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35134">#35134</a>,
<a
href="https://github.com/josephsavona"><code>@​josephsavona</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35147">#35147</a>,
<a href="https://github.com/jackpope"><code>@​jackpope</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35214">#35214</a>,
<a
href="https://github.com/chesnokov-tony"><code>@​chesnokov-tony</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35419">#35419</a>,
<a href="https://github.com/jsleitor"><code>@​jsleitor</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/36107">#36107</a>)</li>
<li>Improved ref validation for non-mutating functions and event handler
props. (<a
href="https://github.com/josephsavona"><code>@​josephsavona</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35893">#35893</a>,
<a href="https://github.com/kolvian"><code>@​kolvian</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35062">#35062</a>)</li>
<li>Compiler now reports all errors instead of stopping at the first.
(<a
href="https://github.com/josephsavona"><code>@​josephsavona</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35873">#35873</a>–<a
href="https://redirect.github.com/facebook/react/pull/35884">#35884</a>)</li>
<li>Improved source locations and error display in compiler diagnostics.
(<a href="https://github.com/nathanmarks"><code>@​nathanmarks</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35348">#35348</a>,
<a
href="https://github.com/josephsavona"><code>@​josephsavona</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/34963">#34963</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md">eslint-plugin-react-hooks's
changelog</a>.</em></p>
<blockquote>
<h2>7.1.0</h2>
<p>This release adds ESLint v10 support, improves performance by
skipping compilation for non-React files, and includes compiler lint
improvements including better <code>set-state-in-effect</code>
detection, improved ref validation, and more helpful error
reporting.</p>
<ul>
<li>Add ESLint v10 support. (<a
href="https://github.com/azat-io"><code>@​azat-io</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35720">#35720</a>)</li>
<li>Skip compilation for non-React files to improve performance. (<a
href="https://github.com/josephsavona"><code>@​josephsavona</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35589">#35589</a>)</li>
<li>Fix exhaustive deps bug with Flow type casting. (<a
href="https://github.com/jorge-cab"><code>@​jorge-cab</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35691">#35691</a>)</li>
<li>Fix <code>useEffectEvent</code> checks in component syntax. (<a
href="https://github.com/jbrown215"><code>@​jbrown215</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35041">#35041</a>)</li>
<li>Improved <code>set-state-in-effect</code> validation with fewer
false negatives. (<a
href="https://github.com/jorge-cab"><code>@​jorge-cab</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35134">#35134</a>,
<a
href="https://github.com/josephsavona"><code>@​josephsavona</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35147">#35147</a>,
<a href="https://github.com/jackpope"><code>@​jackpope</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35214">#35214</a>,
<a
href="https://github.com/chesnokov-tony"><code>@​chesnokov-tony</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35419">#35419</a>,
<a href="https://github.com/jsleitor"><code>@​jsleitor</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/36107">#36107</a>)</li>
<li>Improved ref validation for non-mutating functions and event handler
props. (<a
href="https://github.com/josephsavona"><code>@​josephsavona</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35893">#35893</a>,
<a href="https://github.com/kolvian"><code>@​kolvian</code></a> in <a
href="https://redirect.github.com/facebook/react/pull/35062">#35062</a>)</li>
<li>Compiler now reports all errors instead of stopping at the first.
(<a
href="https://github.com/josephsavona"><code>@​josephsavona</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35873">#35873</a>–<a
href="https://redirect.github.com/facebook/react/pull/35884">#35884</a>)</li>
<li>Improved source locations and error display in compiler diagnostics.
(<a href="https://github.com/nathanmarks"><code>@​nathanmarks</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/35348">#35348</a>,
<a
href="https://github.com/josephsavona"><code>@​josephsavona</code></a>
in <a
href="https://redirect.github.com/facebook/react/pull/34963">#34963</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/facebook/react/commits/eslint-plugin-react-hooks@7.1.0/packages/eslint-plugin-react-hooks">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=eslint-plugin-react-hooks&package-manager=npm_and_yarn&previous-version=7.0.1&new-version=7.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-25 12:33:42 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 609578da0c build(deps-dev): Bump typescript from 6.0.2 to 6.0.3 in the typescript group (#179)
Bumps the typescript group with 1 update:
[typescript](https://github.com/microsoft/TypeScript).

Updates `typescript` from 6.0.2 to 6.0.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/microsoft/TypeScript/releases">typescript's
releases</a>.</em></p>
<blockquote>
<h2>TypeScript 6.0.3</h2>
<p>For release notes, check out the <a
href="https://devblogs.microsoft.com/typescript/announcing-typescript-6-0/">release
announcement blog post</a>.</p>
<ul>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+6.0.0%22">fixed
issues query for TypeScript 6.0.0 (Beta)</a>.</li>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+6.0.1%22">fixed
issues query for TypeScript 6.0.1 (RC)</a>.</li>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+6.0.2%22">fixed
issues query for TypeScript 6.0.2 (Stable)</a>.</li>
<li><a
href="https://github.com/Microsoft/TypeScript/issues?utf8=%E2%9C%93&amp;q=milestone%3A%22TypeScript+6.0.3%22">fixed
issues query for TypeScript 6.0.3 (Stable)</a>.</li>
</ul>
<p>Downloads are available on:</p>
<ul>
<li><a href="https://www.npmjs.com/package/typescript">npm</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/microsoft/TypeScript/commit/050880ce59e30b356b686bd3144efe24f875ebc8"><code>050880c</code></a>
Bump version to 6.0.3 and LKG</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/eeae9dd0f17aa494658e4ec079dc002e02dd625e"><code>eeae9dd</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/63401">#63401</a>
(Also check package name validity in...) into release-6.0 (#...</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/ad1c695fada682764bb510dd680e8f175ae54094"><code>ad1c695</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/63368">#63368</a>
(Harden ATA package name filtering) into release-6.0 (<a
href="https://redirect.github.com/microsoft/TypeScript/issues/63372">#63372</a>)</li>
<li><a
href="https://github.com/microsoft/TypeScript/commit/0725fb4664a1d5ec94040b6d94db77dc1cc354e4"><code>0725fb4</code></a>
🤖 Pick PR <a
href="https://redirect.github.com/microsoft/TypeScript/issues/63310">#63310</a>
(Mark class property initializers as...) into release-6.0 (#...</li>
<li>See full diff in <a
href="https://github.com/microsoft/TypeScript/compare/v6.0.2...v6.0.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typescript&package-manager=npm_and_yarn&previous-version=6.0.2&new-version=6.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-25 12:32:52 +02:00
Martin c3f338780b fix login, unify backend and frontend in 1 vercel project (#180) 2026-04-25 12:24:58 +02:00
Martin c4a4220448 fix backend build (#177) 2026-04-23 21:37:19 +02:00
Martin 9d8a7b7f0c 'plugin-react-swc' -> 'plugin-react' (#168)
I upgraded several dependencies yesterday, including vite. Running `pnpm
run dev:frontend` now prints:
> [vite:react-swc] We recommend switching to `@vitejs/plugin-react` for
improved performance as no swc plugins are used. More information at
https://vite.dev/rolldown

So in this PR I switch to `@vitejs/plugin-react`. Unless you had a
specific reason to use `@vitejs/plugin-react-swc` instead?
2026-04-23 21:27:20 +02:00
Martin 0898dbfb00 re-build lockfile (#176)
Running `pnpm upgrade -r` in my previous PR didn't upgrade everything,
because of a bug in pnpm: https://github.com/pnpm/pnpm/issues/9900

So I deleted the lockfile and node_modules and re-installed everything
in order to upgrade all dependencies.
2026-04-22 21:06:14 +02:00
Martin 6d3145c3d8 run pnpm upgrade (#175) 2026-04-22 20:07:52 +02:00
Martin 5156a811cb exclude 'pg' from frontend build (#169)
closes #142
2026-04-22 19:34:43 +02:00
Martinanddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e263e5dfc4 upgrade pnpm and vite (#174)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 18:50:59 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f8094ab5d3 build(deps-dev): Bump prettier from 3.8.2 to 3.8.3 (#173)
Bumps [prettier](https://github.com/prettier/prettier) from 3.8.2 to
3.8.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/prettier/prettier/releases">prettier's
releases</a>.</em></p>
<blockquote>
<h2>3.8.3</h2>
<ul>
<li>SCSS: Prevent trailing comma in <code>if()</code> function (<a
href="https://redirect.github.com/prettier/prettier/pull/18471">prettier/prettier#18471</a>
by <a href="https://github.com/kovsu"><code>@​kovsu</code></a>)</li>
</ul>
<p>🔗 <a
href="https://github.com/prettier/prettier/blob/3.8.3/CHANGELOG.md#383">Changelog</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md">prettier's
changelog</a>.</em></p>
<blockquote>
<h1>3.8.3</h1>
<p><a
href="https://github.com/prettier/prettier/compare/3.8.2...3.8.3">diff</a></p>
<h4>SCSS: Prevent trailing comma in <code>if()</code> function (<a
href="https://redirect.github.com/prettier/prettier/pull/18471">#18471</a>
by <a href="https://github.com/kovsu"><code>@​kovsu</code></a>)</h4>
<!-- raw HTML omitted -->
<pre lang="scss"><code>// Input
$value: if(sass(false): 1; else: -1);
<p>// Prettier 3.8.2<br />
$value: if(<br />
sass(false): 1; else: -1,<br />
);</p>
<p>// Prettier 3.8.3<br />
$value: if(sass(false): 1; else: -1);<br />
</code></pre></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/prettier/prettier/commit/d7108a79ec745c04292aabf22c4c1adbd690b191"><code>d7108a7</code></a>
Release 3.8.3</li>
<li><a
href="https://github.com/prettier/prettier/commit/177f90898170d363ef64fde663e4d13170688bfe"><code>177f908</code></a>
Prevent trailing comma in SCSS <code>if()</code> function (<a
href="https://redirect.github.com/prettier/prettier/issues/18471">#18471</a>)</li>
<li><a
href="https://github.com/prettier/prettier/commit/1cd40668c3d6f2f4cf9d87bbc9096d92361b2606"><code>1cd4066</code></a>
Release <code>@​prettier/plugin-oxc</code><a
href="https://github.com/0"><code>@​0</code></a>.1.4</li>
<li><a
href="https://github.com/prettier/prettier/commit/a8700e245038cd8cc0cf28ef06ffedbcb3fc2dfc"><code>a8700e2</code></a>
Update oxc-parser to v0.125.0</li>
<li><a
href="https://github.com/prettier/prettier/commit/752157c78eca6f0a30e5d5cb513b682c5ecfa01e"><code>752157c</code></a>
Fix tests</li>
<li><a
href="https://github.com/prettier/prettier/commit/053fd418e180b12fa2014260212fae831f5fc5ec"><code>053fd41</code></a>
Bump Prettier dependency to 3.8.2</li>
<li><a
href="https://github.com/prettier/prettier/commit/904c6365ec46726fd0e21021c52ae934b7e5abc6"><code>904c636</code></a>
Clean changelog_unreleased</li>
<li><a
href="https://github.com/prettier/prettier/commit/dc1f7fcc508d116cbf1644d69a1f0eb93e40d4a4"><code>dc1f7fc</code></a>
Update dependents count</li>
<li>See full diff in <a
href="https://github.com/prettier/prettier/compare/3.8.2...3.8.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=prettier&package-manager=npm_and_yarn&previous-version=3.8.2&new-version=3.8.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 18:24:46 +02:00
Marco Pasqualetti 9c0f4a8390 chore(dependabot): add groups (#171)
Adds groups configuration to the Dependabot `npm` ecosystem block,
consolidating related packages into single PRs instead of one PR per
package
2026-04-22 18:12:08 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1779475e1b build(deps-dev): Bump typescript-eslint from 8.58.1 to 8.58.2 (#166)
Bumps
[typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint)
from 8.58.1 to 8.58.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">typescript-eslint's
releases</a>.</em></p>
<blockquote>
<h2>v8.58.2</h2>
<h2>8.58.2 (2026-04-13)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li>remove tsbuildinfo cache file from published packages (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12187">#12187</a>)</li>
<li><strong>eslint-plugin:</strong> [no-unnecessary-condition] use
assignability checks in checkTypePredicates (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12147">#12147</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Abhijeet Singh <a
href="https://github.com/cseas"><code>@​cseas</code></a></li>
<li>송재욱</li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.58.2">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md">typescript-eslint's
changelog</a>.</em></p>
<blockquote>
<h2>8.58.2 (2026-04-13)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li>remove tsbuildinfo cache file from published packages (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12187">#12187</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Abhijeet Singh <a
href="https://github.com/cseas"><code>@​cseas</code></a></li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.58.2">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/90c2803a4c250e0343598d41e973f95e743bf4ce"><code>90c2803</code></a>
chore(release): publish 8.58.2</li>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/b3315fd7ba53b7da218e5675cd960939b80a76ab"><code>b3315fd</code></a>
chore: convert import eslint to import js - followup (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint/issues/12100">#12100</a>)</li>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/be6b49a02d68db9bdc8985e7e9e1598700fda2fa"><code>be6b49a</code></a>
fix: remove tsbuildinfo cache file from published packages (<a
href="https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint/issues/12187">#12187</a>)</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.2/packages/typescript-eslint">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typescript-eslint&package-manager=npm_and_yarn&previous-version=8.58.1&new-version=8.58.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 14:12:15 +02:00
Martin b474572598 remove node 22 (#170) 2026-04-21 14:11:24 +02:00
martin-mfg 4cabac4bc1 upgrade react to v19 2026-04-21 09:09:30 +02:00
Martinanddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d2a2274eeb build(deps-dev): Bump typescript from 5.9.3 to 6.0.2 (#165)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 20:53:34 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 30e2233ae4 build(deps): Bump vite from 7.3.1 to 8.0.8 (#147)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 7.3.1 to 8.0.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.8</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.8/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.7</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.7/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.6</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.6/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.5</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.5/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.4</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.4/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>create-vite@8.0.3</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/create-vite@8.0.3/packages/create-vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.3</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.3/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>create-vite@8.0.2</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/create-vite@8.0.2/packages/create-vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.2</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.2/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>create-vite@8.0.1</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/create-vite@8.0.1/packages/create-vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.1</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.1/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>plugin-legacy@8.0.1</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/plugin-legacy@8.0.1/packages/plugin-legacy/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>create-vite@8.0.0</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/create-vite@8.0.0/packages/create-vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>plugin-legacy@8.0.0</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/plugin-legacy@8.0.0/packages/plugin-legacy/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.0</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.0/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.0-beta.18</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.0-beta.18/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.0-beta.17</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.0-beta.17/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.7...v8.0.8">8.0.8</a>
(2026-04-09)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li>update rolldown to 1.0.0-rc.15 (<a
href="https://redirect.github.com/vitejs/vite/issues/22201">#22201</a>)
(<a
href="https://github.com/vitejs/vite/commit/6baf587255936e91348cbe624caefd10e8c607ab">6baf587</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>avoid <code>dns.getDefaultResultOrder</code> temporary (<a
href="https://redirect.github.com/vitejs/vite/issues/22202">#22202</a>)
(<a
href="https://github.com/vitejs/vite/commit/15f1c15ac9be343ee4f317fd025c3d67f0c7bd53">15f1c15</a>)</li>
<li><strong>ssr:</strong> class property keys hoisting matching imports
(<a
href="https://redirect.github.com/vitejs/vite/issues/22199">#22199</a>)
(<a
href="https://github.com/vitejs/vite/commit/e1376018cd516d6970534fce495e24f9ee683ce3">e137601</a>)</li>
</ul>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.6...v8.0.7">8.0.7</a>
(2026-04-07)<!-- raw HTML omitted --></h2>
<h3>Bug Fixes</h3>
<ul>
<li>use sync dns.getDefaultResultOrder instead of dns.promises (<a
href="https://redirect.github.com/vitejs/vite/issues/22185">#22185</a>)
(<a
href="https://github.com/vitejs/vite/commit/5c05b04db6f3f64af60ff6525eb5f04bc330fbb5">5c05b04</a>)</li>
</ul>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.5...v8.0.6">8.0.6</a>
(2026-04-07)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li>update rolldown to 1.0.0-rc.13 (<a
href="https://redirect.github.com/vitejs/vite/issues/22097">#22097</a>)
(<a
href="https://github.com/vitejs/vite/commit/51d3e48980bf3e352d31ecde49d1aa56859918e0">51d3e48</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><strong>css:</strong> avoid mutating sass error multiple times (<a
href="https://redirect.github.com/vitejs/vite/issues/22115">#22115</a>)
(<a
href="https://github.com/vitejs/vite/commit/d5081c2f3e73f3fc06956d8cf5f2cf06463c2325">d5081c2</a>)</li>
<li><strong>optimize-deps:</strong> hoist CJS interop assignment (<a
href="https://redirect.github.com/vitejs/vite/issues/22156">#22156</a>)
(<a
href="https://github.com/vitejs/vite/commit/17a8f9edfcd3b9344668120a84e3328aeada3c97">17a8f9e</a>)</li>
</ul>
<h3>Performance Improvements</h3>
<ul>
<li>early return in <code>getLocalhostAddressIfDiffersFromDNS</code>
when DNS order is <code>verbatim</code> (<a
href="https://redirect.github.com/vitejs/vite/issues/22151">#22151</a>)
(<a
href="https://github.com/vitejs/vite/commit/56ec25613dba6cc71911f7af6e545ebcb77ec012">56ec256</a>)</li>
</ul>
<h3>Miscellaneous Chores</h3>
<ul>
<li><strong>create-vite:</strong> remove unnecessary DOM.Iterable (<a
href="https://redirect.github.com/vitejs/vite/issues/22168">#22168</a>)
(<a
href="https://github.com/vitejs/vite/commit/bdc53ab1e67f7e2e000112eeed9c85413ddb0e9e">bdc53ab</a>)</li>
<li>replace remaining prettier script (<a
href="https://redirect.github.com/vitejs/vite/issues/22179">#22179</a>)
(<a
href="https://github.com/vitejs/vite/commit/af71fb26dd15098b9523efc3d8ed04f27553174b">af71fb2</a>)</li>
</ul>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.4...v8.0.5">8.0.5</a>
(2026-04-06)<!-- raw HTML omitted --></h2>
<h3>Bug Fixes</h3>
<ul>
<li>apply server.fs check to env transport (<a
href="https://redirect.github.com/vitejs/vite/issues/22159">#22159</a>)
(<a
href="https://github.com/vitejs/vite/commit/f02d9fde0b195afe3ea2944414186962fbbe41e0">f02d9fd</a>)</li>
<li>avoid path traversal with optimize deps sourcemap handler (<a
href="https://redirect.github.com/vitejs/vite/issues/22161">#22161</a>)
(<a
href="https://github.com/vitejs/vite/commit/79f002f2286c03c88c7b74c511c7f9fc6dc46694">79f002f</a>)</li>
<li>check <code>server.fs</code> after stripping query as well (<a
href="https://redirect.github.com/vitejs/vite/issues/22160">#22160</a>)
(<a
href="https://github.com/vitejs/vite/commit/a9a3df299378d9cbc5f069e3536a369f8188c8ff">a9a3df2</a>)</li>
<li>disallow referencing files outside the package from sourcemap (<a
href="https://redirect.github.com/vitejs/vite/issues/22158">#22158</a>)
(<a
href="https://github.com/vitejs/vite/commit/f05f50173461789e0f1323fe06b51f18ca41c132">f05f501</a>)</li>
</ul>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.3...v8.0.4">8.0.4</a>
(2026-04-06)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li>allow esbuild 0.28 as peer deps (<a
href="https://redirect.github.com/vitejs/vite/issues/22155">#22155</a>)
(<a
href="https://github.com/vitejs/vite/commit/b0da97372f5dba73e78035d1cc7680466ff6cf7f">b0da973</a>)</li>
<li><strong>hmr:</strong> truncate list of files on hmr update (<a
href="https://redirect.github.com/vitejs/vite/issues/21535">#21535</a>)
(<a
href="https://github.com/vitejs/vite/commit/d00e806d7be15ebbfe7875e9244963d80ee8b142">d00e806</a>)</li>
<li><strong>optimizer:</strong> log when dependency scanning or bundling
takes over 1s (<a
href="https://redirect.github.com/vitejs/vite/issues/21797">#21797</a>)
(<a
href="https://github.com/vitejs/vite/commit/f61a1ab33b05dc6f6a7eda6e8bc9c4c5b9aab133">f61a1ab</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vitejs/vite/commit/6e585dcb05a3b159fba7ae57f7faf0b1eca7a390"><code>6e585dc</code></a>
release: v8.0.8</li>
<li><a
href="https://github.com/vitejs/vite/commit/e1376018cd516d6970534fce495e24f9ee683ce3"><code>e137601</code></a>
fix(ssr): class property keys hoisting matching imports (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22199">#22199</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/15f1c15ac9be343ee4f317fd025c3d67f0c7bd53"><code>15f1c15</code></a>
fix: avoid <code>dns.getDefaultResultOrder</code> temporary (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22202">#22202</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/6baf587255936e91348cbe624caefd10e8c607ab"><code>6baf587</code></a>
feat: update rolldown to 1.0.0-rc.15 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22201">#22201</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/fdb2e6f63894d8c458c1778f3df77afe537f2bb2"><code>fdb2e6f</code></a>
release: v8.0.7</li>
<li><a
href="https://github.com/vitejs/vite/commit/5c05b04db6f3f64af60ff6525eb5f04bc330fbb5"><code>5c05b04</code></a>
fix: use sync dns.getDefaultResultOrder instead of dns.promises (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22185">#22185</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/7b3086fae4170252e4cd53f3988f207a943ac5cb"><code>7b3086f</code></a>
release: v8.0.6</li>
<li><a
href="https://github.com/vitejs/vite/commit/af71fb26dd15098b9523efc3d8ed04f27553174b"><code>af71fb2</code></a>
chore: replace remaining prettier script (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22179">#22179</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/51d3e48980bf3e352d31ecde49d1aa56859918e0"><code>51d3e48</code></a>
feat: update rolldown to 1.0.0-rc.13 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22097">#22097</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/17a8f9edfcd3b9344668120a84e3328aeada3c97"><code>17a8f9e</code></a>
fix(optimize-deps): hoist CJS interop assignment (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22156">#22156</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vitejs/vite/commits/v8.0.8/packages/vite">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 20:22:40 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4c2628fa67 build(deps): Bump axios from 1.13.5 to 1.15.0 (#153)
Bumps [axios](https://github.com/axios/axios) from 1.13.5 to 1.15.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/releases">axios's
releases</a>.</em></p>
<blockquote>
<h2>v1.15.0</h2>
<p>This release delivers two critical security patches, adds runtime
support for Deno and Bun, and includes significant CI hardening,
documentation improvements, and routine dependency updates.</p>
<h2>⚠️ Important Changes</h2>
<ul>
<li><strong>Deprecation:</strong> <code>url.parse()</code> usage has
been replaced to address Node.js deprecation warnings. If you are on a
recent version of Node.js, this resolves console warnings you may have
been seeing. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10625">#10625</a></strong>)</li>
</ul>
<h2>🔒 Security Fixes</h2>
<ul>
<li><strong>Proxy Handling:</strong> Fixed a <code>no_proxy</code>
hostname normalisation bypass that could lead to Server-Side Request
Forgery (SSRF). (<strong><a
href="https://redirect.github.com/axios/axios/issues/10661">#10661</a></strong>)</li>
<li><strong>Header Injection:</strong> Fixed an unrestricted cloud
metadata exfiltration vulnerability via a header injection chain.
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10660">#10660</a></strong>)</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li><strong>Runtime Support:</strong> Added compatibility checks and
documentation for Deno and Bun environments. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10652">#10652</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10653">#10653</a></strong>)</li>
</ul>
<h2>🔧 Maintenance &amp; Chores</h2>
<ul>
<li><strong>CI Security:</strong> Hardened workflow permissions to least
privilege, added the <code>zizmor</code> security scanner, pinned action
versions, and gated npm publishing with OIDC and environment protection.
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10618">#10618</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10619">#10619</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10627">#10627</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10637">#10637</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10666">#10666</a></strong>)</li>
<li><strong>Dependencies:</strong> Bumped
<code>serialize-javascript</code>, <code>handlebars</code>,
<code>picomatch</code>, <code>vite</code>, and
<code>denoland/setup-deno</code> to latest versions. Added a 7-day
Dependabot cooldown period. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10574">#10574</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10572">#10572</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10568">#10568</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10663">#10663</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10664">#10664</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10665">#10665</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10669">#10669</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10670">#10670</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10616">#10616</a></strong>)</li>
<li><strong>Documentation:</strong> Unified docs, improved
<code>beforeRedirect</code> credential leakage example, clarified
<code>withCredentials</code>/<code>withXSRFToken</code> behaviour,
HTTP/2 support notes, async/await timeout error handling, header case
preservation, and various typo fixes. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10649">#10649</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10624">#10624</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/7452">#7452</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/7471">#7471</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10654">#10654</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10644">#10644</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10589">#10589</a></strong>)</li>
<li><strong>Housekeeping:</strong> Removed stale files, regenerated
lockfile, and updated sponsor scripts and blocks. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10584">#10584</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10650">#10650</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10582">#10582</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10640">#10640</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10659">#10659</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10668">#10668</a></strong>)</li>
<li><strong>Tests:</strong> Added regression coverage for urlencoded
<code>Content-Type</code> casing. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10573">#10573</a></strong>)</li>
</ul>
<h2>🌟 New Contributors</h2>
<p>We are thrilled to welcome our new contributors. Thank you for
helping improve Axios:</p>
<ul>
<li><strong><a
href="https://github.com/raashish1601"><code>@​raashish1601</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10573">#10573</a></strong>)</li>
<li><strong><a
href="https://github.com/Kilros0817"><code>@​Kilros0817</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10625">#10625</a></strong>)</li>
<li><strong><a
href="https://github.com/ashstrc"><code>@​ashstrc</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10624">#10624</a></strong>)</li>
<li><strong><a
href="https://github.com/Abhi3975"><code>@​Abhi3975</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10589">#10589</a></strong>)</li>
<li><strong><a
href="https://github.com/theamodhshetty"><code>@​theamodhshetty</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/7452">#7452</a></strong>)</li>
</ul>
<h2>v1.14.0</h2>
<p>This release focuses on compatibility fixes, adapter stability
improvements, and test/tooling modernisation.</p>
<h2>⚠️ Important Changes</h2>
<ul>
<li><strong>Breaking Changes:</strong> None identified in this
release.</li>
<li><strong>Action Required:</strong> If you rely on env-based proxy
behaviour or CJS resolution edge-cases, validate your integration after
upgrade (notably <code>proxy-from-env</code> v2 alignment and
<code>main</code> entry compatibility fix).</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li><strong>Runtime Features:</strong> No new end-user features were
introduced in this release.</li>
<li><strong>Test Coverage Expansion:</strong> Added broader smoke/module
test coverage for CJS and ESM package usage. (<a
href="https://redirect.github.com/axios/axios/pull/7510">#7510</a>)</li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li><strong>Headers:</strong> Trim trailing CRLF in normalised header
values. (<a
href="https://redirect.github.com/axios/axios/pull/7456">#7456</a>)</li>
<li><strong>HTTP/2:</strong> Close detached HTTP/2 sessions on timeout
to avoid lingering sessions. (<a
href="https://redirect.github.com/axios/axios/pull/7457">#7457</a>)</li>
<li><strong>Fetch Adapter:</strong> Cancel <code>ReadableStream</code>
created during request-stream capability probing to prevent async
resource leaks. (<a
href="https://redirect.github.com/axios/axios/pull/7515">#7515</a>)</li>
<li><strong>Proxy Handling:</strong> Fixed env proxy behavior with
<code>proxy-from-env</code> v2 usage. (<a
href="https://redirect.github.com/axios/axios/pull/7499">#7499</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/blob/v1.x/CHANGELOG.md">axios's
changelog</a>.</em></p>
<blockquote>
<h2>v1.15.0 — April 7, 2026</h2>
<p>This release delivers two critical security patches targeting header
injection and SSRF via proxy bypass, adds official runtime support for
Deno and Bun, and includes significant CI security hardening.</p>
<h2>🔒 Security Fixes</h2>
<ul>
<li>
<p><strong>Header Injection (CRLF):</strong> Rejects any header value
containing <code>\r</code> or <code>\n</code> characters to block CRLF
injection chains that could be used to exfiltrate cloud metadata (IMDS).
Behavior change: headers with CR/LF now throw <code>&quot;Invalid
character in header content&quot;</code>. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10660">#10660</a></strong>)</p>
</li>
<li>
<p><strong>SSRF via <code>no_proxy</code> Bypass:</strong> Introduces a
<code>shouldBypassProxy</code> helper that normalises hostnames (strips
trailing dots, handles bracketed IPv6) before evaluating
<code>no_proxy</code>/<code>NO_PROXY</code> rules, closing a gap that
could cause loopback or internal hosts to be inadvertently proxied.
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10661">#10661</a></strong>)</p>
</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li><strong>Deno &amp; Bun Runtime Support:</strong> Added full smoke
test suites for Deno and Bun, with CI workflows that run both runtimes
before any release is cut. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10652">#10652</a></strong>)</li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li><strong>Node.js v22 Compatibility:</strong> Replaced deprecated
<code>url.parse()</code> calls with the WHATWG
<code>URL</code>/<code>URLSearchParams</code> API across examples,
sandbox, and tests, eliminating <code>DEP0169</code> deprecation
warnings on Node.js v22+. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10625">#10625</a></strong>)</li>
</ul>
<h2>🔧 Maintenance &amp; Chores</h2>
<ul>
<li>
<p><strong>CI Security Hardening:</strong> Added <a
href="https://github.com/zizmorcore/zizmor">zizmor</a> GitHub Actions
security scanner; switched npm publish to OIDC Trusted Publishing
(removing the long-lived <code>NODE_AUTH_TOKEN</code>); pinned all
action references to full commit SHAs; narrowed workflow permissions to
least privilege; gated the publish step behind a dedicated
<code>npm-publish</code> environment; and blocked the sponsor-block
workflow from running on forks. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10618">#10618</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10619">#10619</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10627">#10627</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10637">#10637</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10641">#10641</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10666">#10666</a></strong>)</p>
</li>
<li>
<p><strong>Docs:</strong> Clarified HTTP/2 support and the unsupported
<code>httpVersion</code> option; added documentation for header case
preservation; improved the <code>beforeRedirect</code> example to
prevent accidental credential leakage. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10644">#10644</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10654">#10654</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10624">#10624</a></strong>)</p>
</li>
<li>
<p><strong>Dependencies:</strong> Bumped <code>picomatch</code>,
<code>handlebars</code>, <code>serialize-javascript</code>,
<code>vite</code> (×3), <code>denoland/setup-deno</code>, and 4
additional dev dependencies to latest versions. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10564">#10564</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10565">#10565</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10567">#10567</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10568">#10568</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10572">#10572</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10574">#10574</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10663">#10663</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10664">#10664</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10665">#10665</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10669">#10669</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10670">#10670</a></strong>)</p>
</li>
</ul>
<h2>🌟 New Contributors</h2>
<p>We are thrilled to welcome our new contributors. Thank you for
helping improve axios:</p>
<ul>
<li><strong><a
href="https://github.com/Kilros0817"><code>@​Kilros0817</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10625">#10625</a></strong>)</li>
<li><strong><a
href="https://github.com/shaanmajid"><code>@​shaanmajid</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10616">#10616</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10617">#10617</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10618">#10618</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10619">#10619</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10637">#10637</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10641">#10641</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10666">#10666</a></strong>)</li>
<li><strong><a
href="https://github.com/ashstrc"><code>@​ashstrc</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10624">#10624</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10644">#10644</a></strong>)</li>
<li><strong><a
href="https://github.com/Abhi3975"><code>@​Abhi3975</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10589">#10589</a></strong>)</li>
<li><strong><a
href="https://github.com/raashish1601"><code>@​raashish1601</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10573">#10573</a></strong>)</li>
</ul>
<p><a
href="https://github.com/axios/axios/compare/v1.14.0...v1.15.0">Full
Changelog</a></p>
<hr />
<h2>v1.14.0 — March 27, 2026</h2>
<p>This release fixes a security vulnerability in the
<code>formidable</code> dependency, resolves a CommonJS compatibility
regression, hardens proxy and HTTP/2 handling, and modernises the build
and test toolchain.</p>
<h2>🔒 Security Fixes</h2>
<ul>
<li><strong>Formidable Vulnerability:</strong> Upgraded
<code>formidable</code> from v2 to v3 to address a reported
arbitrary-file vulnerability. Updated test server and assertions to
align with the v3 API. (<strong><a
href="https://redirect.github.com/axios/axios/issues/7533">#7533</a></strong>)</li>
</ul>
<h2>🐛 Bug Fixes</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/axios/axios/commit/772a4e54ecc4cc2421e2b746daff0aca10f359d7"><code>772a4e5</code></a>
chore(release): prepare release 1.15.0 (<a
href="https://redirect.github.com/axios/axios/issues/10671">#10671</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/4b071371be2f810b4bc7797a13838e0f806ebb22"><code>4b07137</code></a>
chore(deps-dev): bump vite from 8.0.0 to 8.0.5 in /tests/smoke/esm (<a
href="https://redirect.github.com/axios/axios/issues/10663">#10663</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/51e57b39db251bfe3d34af5c943dfea18e06c8b6"><code>51e57b3</code></a>
chore(deps-dev): bump vite from 8.0.2 to 8.0.5 (<a
href="https://redirect.github.com/axios/axios/issues/10664">#10664</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/fba1a77930f0c459677b729161627234b88c90aa"><code>fba1a77</code></a>
chore(deps-dev): bump vite from 8.0.2 to 8.0.5 in /tests/module/esm (<a
href="https://redirect.github.com/axios/axios/issues/10665">#10665</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/0bf6e28eac86e87da2b60bbf5ea4237910e1a08e"><code>0bf6e28</code></a>
chore(deps): bump denoland/setup-deno in the github-actions group (<a
href="https://redirect.github.com/axios/axios/issues/10669">#10669</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/8107157c572ee4a54cb28c01ab7f7f3d895ba661"><code>8107157</code></a>
chore(deps-dev): bump the development_dependencies group with 4 updates
(<a
href="https://redirect.github.com/axios/axios/issues/10670">#10670</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/e66530e3302d56176befd0778155dafea2487542"><code>e66530e</code></a>
ci: require npm-publish environment for releases (<a
href="https://redirect.github.com/axios/axios/issues/10666">#10666</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/49f23cbfe4d308a075281c5f798d4c68f648cbe2"><code>49f23cb</code></a>
chore(sponsor): update sponsor block (<a
href="https://redirect.github.com/axios/axios/issues/10668">#10668</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/363185461b90b1b78845dc8a99a1f103d9b122a1"><code>3631854</code></a>
fix: unrestricted cloud metadata exfiltration via header injection chain
(<a
href="https://redirect.github.com/axios/axios/issues/10">#10</a>...</li>
<li><a
href="https://github.com/axios/axios/commit/fb3befb6daac6cad26b2e54094d0f2d9e47f24df"><code>fb3befb</code></a>
fix: no_proxy hostname normalization bypass leads to ssrf (<a
href="https://redirect.github.com/axios/axios/issues/10661">#10661</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/axios/axios/compare/v1.13.5...v1.15.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Install script changes</summary>
<p>This version modifies <code>prepare</code> script that runs during
installation. Review the package contents before updating.</p>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 19:18:24 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9dcfce415d build(deps-dev): Bump eslint from 10.1.0 to 10.2.0 (#151)
Bumps [eslint](https://github.com/eslint/eslint) from 10.1.0 to 10.2.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/eslint/eslint/releases">eslint's
releases</a>.</em></p>
<blockquote>
<h2>v10.2.0</h2>
<h2>Features</h2>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/586ec2f43092779acc957866db4abe999112d1e1"><code>586ec2f</code></a>
feat: Add <code>meta.languages</code> support to rules (<a
href="https://redirect.github.com/eslint/eslint/issues/20571">#20571</a>)
(Copilot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/14207dee3939dc87cfa8b2fcfc271fff2cfd6471"><code>14207de</code></a>
feat: add <code>Temporal</code> to <code>no-obj-calls</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/20675">#20675</a>)
(Pixel998)</li>
<li><a
href="https://github.com/eslint/eslint/commit/bbb2c93a2b31bd30924f32fe69a9acf41f9dfe35"><code>bbb2c93</code></a>
feat: add Temporal to ES2026 globals (<a
href="https://redirect.github.com/eslint/eslint/issues/20672">#20672</a>)
(Pixel998)</li>
</ul>
<h2>Bug Fixes</h2>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/542cb3e6442a4e6ee3457c799e2a0ee23bef0c6a"><code>542cb3e</code></a>
fix: update first-party dependencies (<a
href="https://redirect.github.com/eslint/eslint/issues/20714">#20714</a>)
(Francesco Trotta)</li>
</ul>
<h2>Documentation</h2>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/a2af743ea60f683d0e0de9d98267c1e7e4f5e412"><code>a2af743</code></a>
docs: add <code>language</code> to configuration objects (<a
href="https://redirect.github.com/eslint/eslint/issues/20712">#20712</a>)
(Francesco Trotta)</li>
<li><a
href="https://github.com/eslint/eslint/commit/845f23f1370892bf07d819497ac518c9e65090d6"><code>845f23f</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/5fbcf5958b897cc4df5d652924d18428db37f7ee"><code>5fbcf59</code></a>
docs: remove <code>sourceType</code> from ts playground link (<a
href="https://redirect.github.com/eslint/eslint/issues/20477">#20477</a>)
(Tanuj Kanti)</li>
<li><a
href="https://github.com/eslint/eslint/commit/8702a474659be786b6b1392e5e7c0c56355ae4a4"><code>8702a47</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/ddeaded2ab36951383ff67c60fb64ec68d29a46a"><code>ddeaded</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="https://github.com/eslint/eslint/commit/2b4496691266547784a7f7ad1989ce53381bab91"><code>2b44966</code></a>
docs: add Major Releases section to Manage Releases (<a
href="https://redirect.github.com/eslint/eslint/issues/20269">#20269</a>)
(Milos Djermanovic)</li>
<li><a
href="https://github.com/eslint/eslint/commit/eab65c700ebb16a6e790910c720450c9908961fd"><code>eab65c7</code></a>
docs: update <code>eslint</code> versions in examples (<a
href="https://redirect.github.com/eslint/eslint/issues/20664">#20664</a>)
(루밀LuMir)</li>
<li><a
href="https://github.com/eslint/eslint/commit/3e4a29903bf31f0998e45ad9128a265bce1edc56"><code>3e4a299</code></a>
docs: update ESM Dependencies policies with note for own-usage packages
(<a
href="https://redirect.github.com/eslint/eslint/issues/20660">#20660</a>)
(Milos Djermanovic)</li>
</ul>
<h2>Chores</h2>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/8120e30f833474f47acc061d24d164e9f022264f"><code>8120e30</code></a>
refactor: extract no unmodified loop condition (<a
href="https://redirect.github.com/eslint/eslint/issues/20679">#20679</a>)
(kuldeep kumar)</li>
<li><a
href="https://github.com/eslint/eslint/commit/46e8469786be1b2bbb522100e1d44624d98d3745"><code>46e8469</code></a>
chore: update dependency markdownlint-cli2 to ^0.22.0 (<a
href="https://redirect.github.com/eslint/eslint/issues/20697">#20697</a>)
(renovate[bot])</li>
<li><a
href="https://github.com/eslint/eslint/commit/01ed3aa68477f81a7188e1498cf4906e02015b7c"><code>01ed3aa</code></a>
test: add unit tests for unicode utilities (<a
href="https://redirect.github.com/eslint/eslint/issues/20622">#20622</a>)
(Manish chaudhary)</li>
<li><a
href="https://github.com/eslint/eslint/commit/811f4930f82ee2b6ac8eae75cade9bed63de0781"><code>811f493</code></a>
ci: remove <code>--legacy-peer-deps</code> from types integration tests
(<a
href="https://redirect.github.com/eslint/eslint/issues/20667">#20667</a>)
(Milos Djermanovic)</li>
<li><a
href="https://github.com/eslint/eslint/commit/6b86fcfc5c75d6a3b8a2cf7bcdb3ef60635a9a03"><code>6b86fcf</code></a>
chore: update dependency npm-run-all2 to v8 (<a
href="https://redirect.github.com/eslint/eslint/issues/20663">#20663</a>)
(renovate[bot])</li>
<li><a
href="https://github.com/eslint/eslint/commit/632c4f83bf32b77981c7d395cacddd1bb172ee25"><code>632c4f8</code></a>
chore: add <code>prettier</code> update commit to
<code>.git-blame-ignore-revs</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/20662">#20662</a>)
(루밀LuMir)</li>
<li><a
href="https://github.com/eslint/eslint/commit/b0b0f21927e03ba092400e3c70d7058f537765c8"><code>b0b0f21</code></a>
chore: update dependency eslint-plugin-regexp to ^3.1.0 (<a
href="https://redirect.github.com/eslint/eslint/issues/20659">#20659</a>)
(Milos Djermanovic)</li>
<li><a
href="https://github.com/eslint/eslint/commit/228a2dd4b272c17f516ee3541f1dd69eca0a8ab0"><code>228a2dd</code></a>
chore: update dependency eslint-plugin-eslint-plugin to ^7.3.2 (<a
href="https://redirect.github.com/eslint/eslint/issues/20661">#20661</a>)
(Milos Djermanovic)</li>
<li><a
href="https://github.com/eslint/eslint/commit/3ab4d7e244df244102de9d0d250b2ff12456a785"><code>3ab4d7e</code></a>
test: Add tests for eslintrc-style keys (<a
href="https://redirect.github.com/eslint/eslint/issues/20645">#20645</a>)
(kuldeep kumar)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/eslint/eslint/commit/000128c29235c5a8c35c7ef817dbccc4e42130ca"><code>000128c</code></a>
10.2.0</li>
<li><a
href="https://github.com/eslint/eslint/commit/1988fad70e361fe1c8df7804930a66e6a9bfc698"><code>1988fad</code></a>
Build: changelog update for 10.2.0</li>
<li><a
href="https://github.com/eslint/eslint/commit/542cb3e6442a4e6ee3457c799e2a0ee23bef0c6a"><code>542cb3e</code></a>
fix: update first-party dependencies (<a
href="https://redirect.github.com/eslint/eslint/issues/20714">#20714</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/a2af743ea60f683d0e0de9d98267c1e7e4f5e412"><code>a2af743</code></a>
docs: add <code>language</code> to configuration objects (<a
href="https://redirect.github.com/eslint/eslint/issues/20712">#20712</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/845f23f1370892bf07d819497ac518c9e65090d6"><code>845f23f</code></a>
docs: Update README</li>
<li><a
href="https://github.com/eslint/eslint/commit/5fbcf5958b897cc4df5d652924d18428db37f7ee"><code>5fbcf59</code></a>
docs: remove <code>sourceType</code> from ts playground link (<a
href="https://redirect.github.com/eslint/eslint/issues/20477">#20477</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/8702a474659be786b6b1392e5e7c0c56355ae4a4"><code>8702a47</code></a>
docs: Update README</li>
<li><a
href="https://github.com/eslint/eslint/commit/ddeaded2ab36951383ff67c60fb64ec68d29a46a"><code>ddeaded</code></a>
docs: Update README</li>
<li><a
href="https://github.com/eslint/eslint/commit/8120e30f833474f47acc061d24d164e9f022264f"><code>8120e30</code></a>
refactor: extract no unmodified loop condition (<a
href="https://redirect.github.com/eslint/eslint/issues/20679">#20679</a>)</li>
<li><a
href="https://github.com/eslint/eslint/commit/46e8469786be1b2bbb522100e1d44624d98d3745"><code>46e8469</code></a>
chore: update dependency markdownlint-cli2 to ^0.22.0 (<a
href="https://redirect.github.com/eslint/eslint/issues/20697">#20697</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/eslint/eslint/compare/v10.1.0...v10.2.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 19:12:46 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ae341596f3 build(deps): Bump pg from 8.18.0 to 8.20.0 (#150)
Bumps
[pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) from
8.18.0 to 8.20.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md">pg's
changelog</a>.</em></p>
<blockquote>
<h2>pg@8.20.0</h2>
<ul>
<li>Add <a
href="https://redirect.github.com/brianc/node-postgres/pull/3620">onConnect</a>
callback to pg.Pool constructor options allowing for async
initialization of newly created &amp; connected pooled clients.</li>
</ul>
<h2>pg@8.19.0</h2>
<ul>
<li><a
href="https://redirect.github.com/brianc/node-postgres/pull/3603">Deprecate
interal query queue</a>.</li>
<li>Pass connection parameters <a
href="https://redirect.github.com/brianc/node-postgres/pull/3602">to
password callback</a>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/brianc/node-postgres/commit/c9070cc8d526fca65780cedc25c1966b57cf7532"><code>c9070cc</code></a>
Publish</li>
<li><a
href="https://github.com/brianc/node-postgres/commit/ad36e3c9e80fae771d636328420cebe7d544baae"><code>ad36e3c</code></a>
fix: typo in deprecation notice for <code>client.query()</code> (<a
href="https://github.com/brianc/node-postgres/tree/HEAD/packages/pg/issues/3618">#3618</a>)</li>
<li><a
href="https://github.com/brianc/node-postgres/commit/f2d7d1146cc87024a5fa503dce13c59ff5196d26"><code>f2d7d11</code></a>
Publish</li>
<li><a
href="https://github.com/brianc/node-postgres/commit/5a4bafc2b06fe4f12e83be723bf783a171d1959b"><code>5a4bafc</code></a>
Deprecate Client's internal query queue (<a
href="https://github.com/brianc/node-postgres/tree/HEAD/packages/pg/issues/3603">#3603</a>)</li>
<li><a
href="https://github.com/brianc/node-postgres/commit/a215bfb5bac4b8b12cd69c9a7f5807eb628a8771"><code>a215bfb</code></a>
Typo fix in PgPass deprecation (funciton) (<a
href="https://github.com/brianc/node-postgres/tree/HEAD/packages/pg/issues/3605">#3605</a>)</li>
<li><a
href="https://github.com/brianc/node-postgres/commit/01e05567207872d22f486881300c1ebc2eb48727"><code>01e0556</code></a>
fix(pg-query-stream): invoke <code>this.callback</code> on cursor
end/error (<a
href="https://github.com/brianc/node-postgres/tree/HEAD/packages/pg/issues/2810">#2810</a>)</li>
<li><a
href="https://github.com/brianc/node-postgres/commit/e6e36920075e2c8b2f9ee5d085c7059b80d39fc8"><code>e6e3692</code></a>
Pass connection parameters to password callback (<a
href="https://github.com/brianc/node-postgres/tree/HEAD/packages/pg/issues/3602">#3602</a>)</li>
<li><a
href="https://github.com/brianc/node-postgres/commit/d80d883944d818234b09f85b3844061ae2d9b906"><code>d80d883</code></a>
test: Fix TLS connection test ending too early</li>
<li><a
href="https://github.com/brianc/node-postgres/commit/f332f283e923d137fa0499d121b79fbaedaf7de1"><code>f332f28</code></a>
fix: Connection timeout handling for native clients in connected state
(<a
href="https://github.com/brianc/node-postgres/tree/HEAD/packages/pg/issues/3512">#3512</a>)</li>
<li><a
href="https://github.com/brianc/node-postgres/commit/b2e9cb13e29f1054ddfa6feba4d27949ec6969ff"><code>b2e9cb1</code></a>
Remove testAsync - its redundant (<a
href="https://github.com/brianc/node-postgres/tree/HEAD/packages/pg/issues/3588">#3588</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/brianc/node-postgres/commits/pg@8.20.0/packages/pg">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 19:10:19 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c63bf09478 build(deps-dev): Bump @types/node from 24.12.0 to 24.12.2 (#148)
Bumps
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)
from 24.12.0 to 24.12.2.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 19:05:45 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c772f6ae56 build(deps-dev): Bump @vitejs/plugin-react-swc from 4.2.3 to 4.3.0 (#129)
Bumps
[@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react-swc)
from 4.2.3 to 4.3.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite-plugin-react/releases"><code>@​vitejs/plugin-react-swc</code>'s
releases</a>.</em></p>
<blockquote>
<h2>plugin-react-swc@4.3.0</h2>
<h3>Add Vite 8 to peerDependencies range <a
href="https://redirect.github.com/vitejs/vite-plugin-react/pull/1142">#1142</a></h3>
<p>This plugin is compatible with Vite 8.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc/CHANGELOG.md"><code>@​vitejs/plugin-react-swc</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>4.3.0 (2026-03-12)</h2>
<h3>Add Vite 8 to peerDependencies range <a
href="https://redirect.github.com/vitejs/vite-plugin-react/pull/1142">#1142</a></h3>
<p>This plugin is compatible with Vite 8.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/vitejs/vite-plugin-react/commits/v4.3.0/packages/plugin-react-swc">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:54:56 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d5e7217b4f build(deps-dev): Bump jsdom from 28.1.0 to 29.0.2 (#145)
Bumps [jsdom](https://github.com/jsdom/jsdom) from 28.1.0 to 29.0.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/jsdom/jsdom/releases">jsdom's
releases</a>.</em></p>
<blockquote>
<h2>v29.0.2</h2>
<ul>
<li>Significantly improved and sped up <code>getComputedStyle()</code>.
Computed value rules are now applied across a broader set of properties,
and include fixes related to inheritance, defaulting keywords, custom
properties, and color-related values such as <code>currentcolor</code>
and system colors. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
<li>Fixed CSS <code>'background</code>' and <code>'border'</code>
shorthand parsing. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
</ul>
<h2>v29.0.1</h2>
<ul>
<li>Fixed CSS parsing of <code>'border'</code>,
<code>'background'</code>, and their sub-shorthands containing keywords
or <code>var()</code>. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
<li>Fixed <code>getComputedStyle()</code> to return a more functional
<code>CSSStyleDeclaration</code> object, including indexed access
support, which regressed in v29.0.0.</li>
</ul>
<h2>v29.0.0</h2>
<p>Breaking changes:</p>
<ul>
<li>Node.js v22.13.0+ is now the minimum supported v22 version (was
v22.12.0+).</li>
</ul>
<p>Other changes:</p>
<ul>
<li>Overhauled the CSSOM implementation, replacing the <a
href="https://www.npmjs.com/package/@acemir/cssom"><code>@acemir/cssom</code></a>
and <a
href="https://github.com/jsdom/cssstyle"><code>cssstyle</code></a>
dependencies with fresh internal implementations built on webidl2js
wrappers and the <a
href="https://www.npmjs.com/package/css-tree"><code>css-tree</code></a>
parser. Serialization, parsing, and API behavior is improved in various
ways, especially around edge cases.</li>
<li>Added <code>CSSCounterStyleRule</code> and
<code>CSSNamespaceRule</code> to jsdom <code>Window</code>s.</li>
<li>Added <code>cssMediaRule.matches</code> and
<code>cssSupportsRule.matches</code> getters.</li>
<li>Added proper media query parsing in <code>MediaList</code>, using
<code>css-tree</code> instead of naive comma-splitting. Invalid queries
become <code>&quot;not all&quot;</code> per spec.</li>
<li>Added <code>cssKeyframeRule.keyText</code> getter/setter
validation.</li>
<li>Added <code>cssStyleRule.selectorText</code> setter validation:
invalid selectors are now rejected.</li>
<li>Added <code>styleSheet.ownerNode</code>,
<code>styleSheet.href</code>, and <code>styleSheet.title</code>.</li>
<li>Added bad port blocking per the <a
href="https://fetch.spec.whatwg.org/#bad-port">fetch specification</a>,
preventing fetches to commonly-abused ports.</li>
<li>Improved <code>Document</code> initialization performance by lazily
initializing the CSS selector engine, avoiding ~0.5 ms of overhead per
<code>Document</code>. (<a
href="https://github.com/thypon"><code>@​thypon</code></a>)</li>
<li>Fixed a memory leak when stylesheets were removed from the
document.</li>
<li>Fixed <code>CSSStyleDeclaration</code> modifications to properly
trigger custom element reactions.</li>
<li>Fixed nested <code>@media</code> rule parsing.</li>
<li>Fixed <code>CSSStyleSheet</code>'s &quot;disallow modification&quot;
flag not being checked in all mutation methods.</li>
<li>Fixed <code>XMLHttpRequest</code>'s <code>response</code> getter
returning parsed JSON during the <code>LOADING</code> state instead of
<code>null</code>.</li>
<li>Fixed <code>getComputedStyle()</code> crashing in XHTML documents
when stylesheets contained at-rules such as <code>@page</code> or
<code>@font-face</code>.</li>
<li>Fixed a potential hang in synchronous <code>XMLHttpRequest</code>
caused by a race condition with the worker thread's idle timeout.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/jsdom/jsdom/commit/2a1e2cdb443e276b28ca49937cf7a7d6fa0a4806"><code>2a1e2cd</code></a>
29.0.2</li>
<li><a
href="https://github.com/jsdom/jsdom/commit/4097d66ba188805a408b5186b4b8036bf582b40f"><code>4097d66</code></a>
Resolve computed CSS values lazily in CSSStyleDeclaration</li>
<li><a
href="https://github.com/jsdom/jsdom/commit/cf5523ff92978fc08f7e40c0f44053946101d064"><code>cf5523f</code></a>
Add more test cases for nested color-mix with currentColor</li>
<li><a
href="https://github.com/jsdom/jsdom/commit/b33b61657e563ec841cb7a29c98ad236cc71c0b0"><code>b33b616</code></a>
Add test that getComputedStyle() works with !important</li>
<li><a
href="https://github.com/jsdom/jsdom/commit/6bf559c416654641d3ca197982d646eac6de85ae"><code>6bf559c</code></a>
Add test for custom property inheritance in computed styles</li>
<li><a
href="https://github.com/jsdom/jsdom/commit/68176571d00c50441fc3a428e1315fc1fb3b2bab"><code>6817657</code></a>
Fix border shorthand handling</li>
<li><a
href="https://github.com/jsdom/jsdom/commit/470f5c5943d001f9abf8396734fc0c6f11174784"><code>470f5c5</code></a>
Consolidate color helpers</li>
<li><a
href="https://github.com/jsdom/jsdom/commit/3db53cb2cb14aef35717cbec75b6bd638b1224a7"><code>3db53cb</code></a>
Fix background shorthand handlers</li>
<li><a
href="https://github.com/jsdom/jsdom/commit/678e84093ad44b444be6e0317b84396832ff39c6"><code>678e840</code></a>
Remove some longhand property files</li>
<li><a
href="https://github.com/jsdom/jsdom/commit/d526a07432982dcb89597e469c0fb00ba54884bb"><code>d526a07</code></a>
Add regression test for getComputedStyle() liveness</li>
<li>Additional commits viewable in <a
href="https://github.com/jsdom/jsdom/compare/v28.1.0...v29.0.2">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by [GitHub Actions](<a
href="https://www.npmjs.com/~GitHub">https://www.npmjs.com/~GitHub</a>
Actions), a new releaser for jsdom since your current version.</p>
</details>
<details>
<summary>Install script changes</summary>
<p>This version modifies <code>prepare</code> script that runs during
installation. Review the package contents before updating.</p>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:46:33 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 672777a7e1 build(deps-dev): Bump typescript-eslint from 8.58.0 to 8.58.1 (#157)
Bumps
[typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint)
from 8.58.0 to 8.58.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/releases">typescript-eslint's
releases</a>.</em></p>
<blockquote>
<h2>v8.58.1</h2>
<h2>8.58.1 (2026-04-08)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>eslint-plugin:</strong> [no-unused-vars] fix false negative
for type predicate parameter (<a
href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12004">#12004</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>MinJae <a
href="https://github.com/Ju-MINJAE"><code>@​Ju-MINJAE</code></a></li>
</ul>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.58.1">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md">typescript-eslint's
changelog</a>.</em></p>
<blockquote>
<h2>8.58.1 (2026-04-08)</h2>
<p>This was a version bump only for typescript-eslint to align it with
other projects, there were no code changes.</p>
<p>See <a
href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.58.1">GitHub
Releases</a> for more information.</p>
<p>You can read about our <a
href="https://typescript-eslint.io/users/versioning">versioning
strategy</a> and <a
href="https://typescript-eslint.io/users/releases">releases</a> on our
website.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/typescript-eslint/typescript-eslint/commit/5311ed312eadf4e238324f2726ae0b1f3f2206e6"><code>5311ed3</code></a>
chore(release): publish 8.58.1</li>
<li>See full diff in <a
href="https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.1/packages/typescript-eslint">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:43:11 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3380ee97cc build(deps): Bump @tailwindcss/vite from 4.2.1 to 4.2.2 (#155)
Bumps
[@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite)
from 4.2.1 to 4.2.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tailwindlabs/tailwindcss/releases"><code>@​tailwindcss/vite</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v4.2.2</h2>
<h3>Added</h3>
<ul>
<li>Support Vite 8 in <code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19790">#19790</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Don't crash when candidates contain prototype properties like
<code>row-constructor</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19725">#19725</a>)</li>
<li>Canonicalize <code>calc(var(--spacing)*…)</code> expressions into
<code>--spacing(…)</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19769">#19769</a>)</li>
<li>Fix crash in canonicalization step when handling utilities
containing <code>@property</code> at-rules (e.g. <code>shadow-sm
border</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19727">#19727</a>)</li>
<li>Skip full reload for server only modules scanned by client CSS when
using <code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19745">#19745</a>)</li>
<li>Improve canonicalization for bare values exceeding default spacing
scale suggestions (e.g. <code>w-1234 h-1234</code> →
<code>size-1234</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19809">#19809</a>)</li>
<li>Fix canonicalization resulting in empty list (e.g. <code>w-5 h-5
size-5</code> → <code>''</code> instead of <code>size-5</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19812">#19812</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md"><code>@​tailwindcss/vite</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>[4.2.2] - 2026-03-18</h2>
<h3>Fixed</h3>
<ul>
<li>Don't crash when candidates contain prototype properties like
<code>row-constructor</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19725">#19725</a>)</li>
<li>Canonicalize <code>calc(var(--spacing)*…)</code> expressions into
<code>--spacing(…)</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19769">#19769</a>)</li>
<li>Fix crash in canonicalization step when handling utilities
containing <code>@property</code> at-rules (e.g. <code>shadow-sm
border</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19727">#19727</a>)</li>
<li>Skip full reload for server only modules scanned by client CSS when
using <code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19745">#19745</a>)</li>
<li>Add support for Vite 8 in <code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19790">#19790</a>)</li>
<li>Improve canonicalization for bare values exceeding default spacing
scale suggestions (e.g. <code>w-1234 h-1234</code> →
<code>size-1234</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19809">#19809</a>)</li>
<li>Fix canonicalization resulting in empty list (e.g. <code>w-5 h-5
size-5</code> → <code>''</code> instead of <code>size-5</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19812">#19812</a>)</li>
<li>Resolve tsconfig paths to allow for <code>@import
'@/path/to/file';</code> when using <code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19803">#19803</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/d596b0c43d36ad5099c983930fb155e089cbc291"><code>d596b0c</code></a>
4.2.2 (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite/issues/19821">#19821</a>)</li>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/faa5e8849be45e1009dc1d0f862ca782ee0a1ee9"><code>faa5e88</code></a>
Cleanup inconsistencies related to (regex) escapes (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite/issues/19804">#19804</a>)</li>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/59b0329f858dd8a25f64dfa55fb971aa6e74c32a"><code>59b0329</code></a>
Add support for Vite 8 in <code>@tailwindcss/vite</code> (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite/issues/19790">#19790</a>)</li>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/bf441a799f67df1596c2cf40eda30ddb50859607"><code>bf441a7</code></a>
fix(vite): skip full reload for server only modules scanned by client
css (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite/issues/1">#1</a>...</li>
<li>See full diff in <a
href="https://github.com/tailwindlabs/tailwindcss/commits/v4.2.2/packages/@tailwindcss-vite">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:41:54 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> baf45b7e21 build(deps): Bump axios-cache-interceptor from 1.11.4 to 1.12.0 (#160)
Bumps
[axios-cache-interceptor](https://github.com/arthurfiorette/axios-cache-interceptor)
from 1.11.4 to 1.12.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/releases">axios-cache-interceptor's
releases</a>.</em></p>
<blockquote>
<h2>v1.12.0</h2>
<h2>What's Changed</h2>
<ul>
<li>build(deps-dev): bump tsdown from 0.19.0 to 0.20.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1173">arthurfiorette/axios-cache-interceptor#1173</a></li>
<li>build(deps-dev): bump <code>@​biomejs/biome</code> from 2.3.11 to
2.3.12 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1175">arthurfiorette/axios-cache-interceptor#1175</a></li>
<li>build(deps-dev): bump <code>@​biomejs/biome</code> from 2.3.12 to
2.3.13 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1177">arthurfiorette/axios-cache-interceptor#1177</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 24.10.9 to
24.10.10 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1180">arthurfiorette/axios-cache-interceptor#1180</a></li>
<li>Move tests to TS native by <a
href="https://github.com/arthurfiorette"><code>@​arthurfiorette</code></a>
in <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1197">arthurfiorette/axios-cache-interceptor#1197</a></li>
<li>fix: log debug message when non-Axios adapter errors leave deferred
unsettled by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1196">arthurfiorette/axios-cache-interceptor#1196</a></li>
<li>build(deps): bump rollup from 4.46.2 to 4.59.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1194">arthurfiorette/axios-cache-interceptor#1194</a></li>
<li>feat: add optional manual interceptor registration by <a
href="https://github.com/arthurfiorette"><code>@​arthurfiorette</code></a>
in <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1199">arthurfiorette/axios-cache-interceptor#1199</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/compare/v1.11.4...v1.12.0">https://github.com/arthurfiorette/axios-cache-interceptor/compare/v1.11.4...v1.12.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/blob/main/CHANGELOG.md">axios-cache-interceptor's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/arthurfiorette/axios-cache-interceptor/compare/v1.11.4...v1.12.0">v1.12.0</a></h2>
<h3>Merged</h3>
<ul>
<li>build(deps): bump rollup from 4.46.2 to 4.59.0 <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1194"><code>[#1194](https://github.com/arthurfiorette/axios-cache-interceptor/issues/1194)</code></a></li>
<li>fix: log debug message when non-Axios adapter errors leave deferred
unsettled <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1196"><code>[#1196](https://github.com/arthurfiorette/axios-cache-interceptor/issues/1196)</code></a></li>
<li>Move tests to TS native <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1197"><code>[#1197](https://github.com/arthurfiorette/axios-cache-interceptor/issues/1197)</code></a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 24.10.9 to
24.10.10 <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1180"><code>[#1180](https://github.com/arthurfiorette/axios-cache-interceptor/issues/1180)</code></a></li>
<li>build(deps-dev): bump <code>@​biomejs/biome</code> from 2.3.12 to
2.3.13 <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1177"><code>[#1177](https://github.com/arthurfiorette/axios-cache-interceptor/issues/1177)</code></a></li>
<li>build(deps-dev): bump <code>@​biomejs/biome</code> from 2.3.11 to
2.3.12 <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1175"><code>[#1175](https://github.com/arthurfiorette/axios-cache-interceptor/issues/1175)</code></a></li>
<li>build(deps-dev): bump tsdown from 0.19.0 to 0.20.1 <a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/pull/1173"><code>[#1173](https://github.com/arthurfiorette/axios-cache-interceptor/issues/1173)</code></a></li>
</ul>
<h3>Commits</h3>
<ul>
<li>Add Serena and AGENTS.md files <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/5c962bc34509e367a3504a8cdad79dee3836050f"><code>5c962bc</code></a></li>
<li>Add URL and public key to context7.json <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/e67ea83035253d7a3d174db93fcfccb0b4d0c7ef"><code>e67ea83</code></a></li>
<li>code <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/f826f2fc9c55ea1e1dbce1582f07a3b3d85e72dd"><code>f826f2f</code></a></li>
<li>Fix formatting <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/440d7b520c71982251e34be94b5589eab3f53ec6"><code>440d7b5</code></a></li>
<li>Improvements to documentation <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/8deb45a681ad1507aaa3f55664359f6fc592dbad"><code>8deb45a</code></a></li>
<li>Pin nodejs version to 24.13 <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/bda5670968ece8a350ea33fd12fadf889dccb351"><code>bda5670</code></a></li>
<li>Update changelog <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/9a575831849f31b6bc562c98ab82acae02b12409"><code>9a57583</code></a></li>
<li>Update dependencies <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/956ff1f86f767ff354bcfc29b1ddce42dade878e"><code>956ff1f</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/8ab3e8ccc705c9f18c85c610d491939285e38b15"><code>8ab3e8c</code></a>
1.12.0</li>
<li><a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/71c5ebac146da451dfc2f977f145a69fe4ce19ea"><code>71c5eba</code></a>
feat: add optional manual interceptor registration (<a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/issues/1199">#1199</a>)</li>
<li><a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/4636ceba1ec25ec5620bfa25763d9f4090f61195"><code>4636ceb</code></a>
build(deps): bump rollup from 4.46.2 to 4.59.0 (<a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/issues/1194">#1194</a>)</li>
<li><a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/2c2268f4fa1a9fe52ee9125fba737784b8ecf040"><code>2c2268f</code></a>
fix: log debug message when non-Axios adapter errors leave deferred
unsettled...</li>
<li><a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/9a575831849f31b6bc562c98ab82acae02b12409"><code>9a57583</code></a>
Update changelog</li>
<li><a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/86b0f56727d0a816726e01026a56e35b823a8e73"><code>86b0f56</code></a>
Move tests to TS native (<a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/issues/1197">#1197</a>)</li>
<li><a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/bda5670968ece8a350ea33fd12fadf889dccb351"><code>bda5670</code></a>
Pin nodejs version to 24.13</li>
<li><a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/f826f2fc9c55ea1e1dbce1582f07a3b3d85e72dd"><code>f826f2f</code></a>
code</li>
<li><a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/9a296504f3b4ef61c8aea787c4078cf01d35ff91"><code>9a29650</code></a>
build(deps-dev): bump <code>@​types/node</code> from 24.10.9 to 24.10.10
(<a
href="https://redirect.github.com/arthurfiorette/axios-cache-interceptor/issues/1180">#1180</a>)</li>
<li><a
href="https://github.com/arthurfiorette/axios-cache-interceptor/commit/8deb45a681ad1507aaa3f55664359f6fc592dbad"><code>8deb45a</code></a>
Improvements to documentation</li>
<li>Additional commits viewable in <a
href="https://github.com/arthurfiorette/axios-cache-interceptor/compare/v1.11.4...v1.12.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=axios-cache-interceptor&package-manager=npm_and_yarn&previous-version=1.11.4&new-version=1.12.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:18:25 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fc4dc9d010 ci(deps): Bump peter-evans/create-pull-request from 8.1.0 to 8.1.1 (#144)
Bumps
[peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request)
from 8.1.0 to 8.1.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/peter-evans/create-pull-request/releases">peter-evans/create-pull-request's
releases</a>.</em></p>
<blockquote>
<h2>Create Pull Request v8.1.1</h2>
<h2>What's Changed</h2>
<ul>
<li>build(deps-dev): bump the npm group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4305">peter-evans/create-pull-request#4305</a></li>
<li>build(deps): bump minimatch by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4311">peter-evans/create-pull-request#4311</a></li>
<li>build(deps): bump the github-actions group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4316">peter-evans/create-pull-request#4316</a></li>
<li>build(deps): bump <code>@​tootallnate/once</code> and
jest-environment-jsdom by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4323">peter-evans/create-pull-request#4323</a></li>
<li>build(deps-dev): bump undici from 6.23.0 to 6.24.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4328">peter-evans/create-pull-request#4328</a></li>
<li>build(deps-dev): bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4334">peter-evans/create-pull-request#4334</a></li>
<li>build(deps): bump picomatch by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4339">peter-evans/create-pull-request#4339</a></li>
<li>build(deps-dev): bump handlebars from 4.7.8 to 4.7.9 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4344">peter-evans/create-pull-request#4344</a></li>
<li>build(deps-dev): bump the npm group with 3 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4349">peter-evans/create-pull-request#4349</a></li>
<li>fix: retry post-creation API calls on 422 eventual consistency
errors by <a
href="https://github.com/peter-evans"><code>@​peter-evans</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/4356">peter-evans/create-pull-request#4356</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/peter-evans/create-pull-request/compare/v8.1.0...v8.1.1">https://github.com/peter-evans/create-pull-request/compare/v8.1.0...v8.1.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/5f6978faf089d4d20b00c7766989d076bb2fc7f1"><code>5f6978f</code></a>
fix: retry post-creation API calls on 422 eventual consistency errors
(<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4356">#4356</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/d32e88dac789dcc7906e7d26f69f24116fa9c97d"><code>d32e88d</code></a>
build(deps-dev): bump the npm group with 3 updates (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4349">#4349</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/8170bccad11c0df62542c04dcaefe36d342dfd39"><code>8170bcc</code></a>
build(deps-dev): bump handlebars from 4.7.8 to 4.7.9 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4344">#4344</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/00418193b417f888dbf1d993c5c0d31d27fdc7de"><code>0041819</code></a>
build(deps): bump picomatch (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4339">#4339</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/b993918c8536b6d44706130734d5456879762b27"><code>b993918</code></a>
build(deps-dev): bump flatted from 3.3.1 to 3.4.2 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4334">#4334</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/36d7c8468b48f9c2f8f29e260e82f10d4b90d2bd"><code>36d7c84</code></a>
build(deps-dev): bump undici from 6.23.0 to 6.24.0 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4328">#4328</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/a45d1fb447fcaf601166e405fd4f335cde1a8aa8"><code>a45d1fb</code></a>
build(deps): bump <code>@​tootallnate/once</code> and
jest-environment-jsdom (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4323">#4323</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/3499eb61835cc0015c0b786e203d74b1e8f55e43"><code>3499eb6</code></a>
build(deps): bump the github-actions group with 2 updates (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4316">#4316</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/3f3b473b8c148f5a7520efb4d1f9a70eea3d9d1f"><code>3f3b473</code></a>
build(deps): bump minimatch (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4311">#4311</a>)</li>
<li><a
href="https://github.com/peter-evans/create-pull-request/commit/6699836a213cf8b28c4f0408a404a6ac79d4458a"><code>6699836</code></a>
build(deps-dev): bump the npm group with 2 updates (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/4305">#4305</a>)</li>
<li>See full diff in <a
href="https://github.com/peter-evans/create-pull-request/compare/c0f553fe549906ede9cf27b5156039d195d2ece0...5f6978faf089d4d20b00c7766989d076bb2fc7f1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=peter-evans/create-pull-request&package-manager=github_actions&previous-version=8.1.0&new-version=8.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:13:17 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 805e668715 build(deps-dev): Bump prettier from 3.8.1 to 3.8.2 (#154)
Bumps [prettier](https://github.com/prettier/prettier) from 3.8.1 to
3.8.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/prettier/prettier/releases">prettier's
releases</a>.</em></p>
<blockquote>
<h2>3.8.2</h2>
<ul>
<li>Support Angular v21.2</li>
</ul>
<p>🔗 <a
href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md#382">Changelog</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md">prettier's
changelog</a>.</em></p>
<blockquote>
<h1>3.8.2</h1>
<p><a
href="https://github.com/prettier/prettier/compare/3.8.1...3.8.2">diff</a></p>
<h4>Angular: Support Angular v21.2 (<a
href="https://redirect.github.com/prettier/prettier/pull/18722">#18722</a>,
<a
href="https://redirect.github.com/prettier/prettier/pull/19034">#19034</a>
by <a href="https://github.com/fisker"><code>@​fisker</code></a>)</h4>
<p>Exhaustive typechecking with <code>@default never;</code></p>
<!-- raw HTML omitted -->
<pre lang="html"><code>&lt;!-- Input --&gt;
@switch (foo) {
  @case (1) {}
  @default never;
}
<p>&lt;!-- Prettier 3.8.1 --&gt;
SyntaxError: Incomplete block &quot;default never&quot;. If you meant to
write the @ character, you should use the &quot;&amp;<a
href="https://redirect.github.com/prettier/prettier/issues/64">#64</a>;&quot;
HTML entity instead. (3:3)</p>
<p>&lt;!-- Prettier 3.8.2 --&gt;
<a href="https://github.com/switch"><code>@​switch</code></a> (foo) {
<a href="https://github.com/case"><code>@​case</code></a> (1) {}
<a href="https://github.com/default"><code>@​default</code></a> never;
}
</code></pre></p>
<p><code>arrow function</code> and <code>instanceof</code>
expressions.</p>
<!-- raw HTML omitted -->
<pre lang="html"><code>&lt;!-- Input --&gt;
@let fn = (a) =&gt;        a?    1:2;
<p>{{ fn ( a         instanceof b)}}</p>
<p>&lt;!-- Prettier 3.8.1 --&gt;
<a href="https://github.com/let"><code>@​let</code></a> fn = (a) =&gt;
a? 1:2;</p>
<p>{{ fn ( a         instanceof b)}}</p>
<p>&lt;!-- Prettier 3.8.2 --&gt;
<a href="https://github.com/let"><code>@​let</code></a> fn = (a) =&gt;
(a ? 1 : 2);</p>
<p>{{ fn(a instanceof b) }}
</code></pre></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/prettier/prettier/commit/b31557cf331a02acf83e7e29d1001b070189a0d9"><code>b31557c</code></a>
Release 3.8.2</li>
<li><a
href="https://github.com/prettier/prettier/commit/96bbaeda0525bf758e464aed2f939d739a85c315"><code>96bbaed</code></a>
Support Angular v21.2 (<a
href="https://redirect.github.com/prettier/prettier/issues/18722">#18722</a>)</li>
<li><a
href="https://github.com/prettier/prettier/commit/881360b92b3cfa39ffa0c0ee03bb4319079849a6"><code>881360b</code></a>
Support <code>default never</code> in Angular v21.2 (<a
href="https://redirect.github.com/prettier/prettier/issues/19034">#19034</a>)</li>
<li><a
href="https://github.com/prettier/prettier/commit/07d67240f7a0f2d65014e00395b342bbec5db31b"><code>07d6724</code></a>
Bump Prettier dependency to 3.8.1</li>
<li><a
href="https://github.com/prettier/prettier/commit/8b4a53ab3daddfe9c10c52fd5785a6a718770c34"><code>8b4a53a</code></a>
Clean changelog_unreleased</li>
<li>See full diff in <a
href="https://github.com/prettier/prettier/compare/3.8.1...3.8.2">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:11:33 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0e45db3041 build(deps-dev): Bump @eslint/compat from 2.0.3 to 2.0.5 (#152)
Bumps
[@eslint/compat](https://github.com/eslint/rewrite/tree/HEAD/packages/compat)
from 2.0.3 to 2.0.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/eslint/rewrite/releases"><code>@​eslint/compat</code>'s
releases</a>.</em></p>
<blockquote>
<h2>migrate-config: v2.0.5</h2>
<h2><a
href="https://github.com/eslint/rewrite/compare/migrate-config-v2.0.4...migrate-config-v2.0.5">2.0.5</a>
(2026-04-03)</h2>
<h3>Dependencies</h3>
<ul>
<li>The following workspace dependencies were updated
<ul>
<li>dependencies
<ul>
<li><code>@​eslint/compat</code> bumped from ^2.0.3 to ^2.0.4</li>
</ul>
</li>
<li>devDependencies
<ul>
<li><code>@​eslint/core</code> bumped from ^1.1.1 to ^1.2.0</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2>compat: v2.0.5</h2>
<h2><a
href="https://github.com/eslint/rewrite/compare/compat-v2.0.4...compat-v2.0.5">2.0.5</a>
(2026-04-08)</h2>
<h3>Dependencies</h3>
<ul>
<li>The following workspace dependencies were updated
<ul>
<li>dependencies
<ul>
<li><code>@​eslint/core</code> bumped from ^1.2.0 to ^1.2.1</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2>migrate-config: v2.0.4</h2>
<h2><a
href="https://github.com/eslint/rewrite/compare/migrate-config-v2.0.3...migrate-config-v2.0.4">2.0.4</a>
(2026-03-20)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>update dependency <code>@​eslint/eslintrc</code> to ^3.3.5 (<a
href="https://redirect.github.com/eslint/rewrite/issues/397">#397</a>)
(<a
href="https://github.com/eslint/rewrite/commit/8567c1969f82f5582f86d9202058ac7e41c0d53d">8567c19</a>)</li>
</ul>
<h2>compat: v2.0.4</h2>
<h2><a
href="https://github.com/eslint/rewrite/compare/compat-v2.0.3...compat-v2.0.4">2.0.4</a>
(2026-04-03)</h2>
<h3>Dependencies</h3>
<ul>
<li>The following workspace dependencies were updated
<ul>
<li>dependencies
<ul>
<li><code>@​eslint/core</code> bumped from ^1.1.1 to ^1.2.0</li>
</ul>
</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/eslint/rewrite/blob/main/packages/compat/CHANGELOG.md"><code>@​eslint/compat</code>'s
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/eslint/rewrite/compare/compat-v2.0.4...compat-v2.0.5">2.0.5</a>
(2026-04-08)</h2>
<h3>Dependencies</h3>
<ul>
<li>The following workspace dependencies were updated
<ul>
<li>dependencies
<ul>
<li><code>@​eslint/core</code> bumped from ^1.2.0 to ^1.2.1</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2><a
href="https://github.com/eslint/rewrite/compare/compat-v2.0.3...compat-v2.0.4">2.0.4</a>
(2026-04-03)</h2>
<h3>Dependencies</h3>
<ul>
<li>The following workspace dependencies were updated
<ul>
<li>dependencies
<ul>
<li><code>@​eslint/core</code> bumped from ^1.1.1 to ^1.2.0</li>
</ul>
</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/eslint/rewrite/commit/d2dbf7b73d01505da89a69b7465e486d8a88aa8f"><code>d2dbf7b</code></a>
chore: release main (<a
href="https://github.com/eslint/rewrite/tree/HEAD/packages/compat/issues/424">#424</a>)</li>
<li><a
href="https://github.com/eslint/rewrite/commit/fe114eed69c5d59d0dd05cda8071a5b98c4daec2"><code>fe114ee</code></a>
chore: release main (<a
href="https://github.com/eslint/rewrite/tree/HEAD/packages/compat/issues/413">#413</a>)</li>
<li><a
href="https://github.com/eslint/rewrite/commit/8863791972f3074b43f3490989e10e293f6295f5"><code>8863791</code></a>
docs: Update README sponsors</li>
<li><a
href="https://github.com/eslint/rewrite/commit/835ddf95b0c421a138b0038f3e38591df9a3fd69"><code>835ddf9</code></a>
docs: Update README sponsors</li>
<li><a
href="https://github.com/eslint/rewrite/commit/8cd3676402f85db8327946a909a7b9c9e3be978f"><code>8cd3676</code></a>
docs: Update README sponsors</li>
<li><a
href="https://github.com/eslint/rewrite/commit/4d734592f3436cf564802a36d4af571e2933fba6"><code>4d73459</code></a>
docs: Update README sponsors</li>
<li><a
href="https://github.com/eslint/rewrite/commit/a6c7a26a852f1606ce10c4642d43ee264f725781"><code>a6c7a26</code></a>
chore: update <code>eslint</code> and <code>eslint-config-eslint</code>
(<a
href="https://github.com/eslint/rewrite/tree/HEAD/packages/compat/issues/401">#401</a>)</li>
<li>See full diff in <a
href="https://github.com/eslint/rewrite/commits/compat-v2.0.5/packages/compat">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@eslint/compat&package-manager=npm_and_yarn&previous-version=2.0.3&new-version=2.0.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:09:07 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b29d7989eb build(deps-dev): Bump turbo from 2.9.3 to 2.9.6 (#149)
Bumps [turbo](https://github.com/vercel/turborepo) from 2.9.3 to 2.9.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vercel/turborepo/releases">turbo's
releases</a>.</em></p>
<blockquote>
<h2>Turborepo v2.9.6</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>create-turbo</h3>
<ul>
<li>chore: Update dependencies found in audits by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12586">vercel/turborepo#12586</a></li>
</ul>
<h3>Examples</h3>
<ul>
<li>fix: Add missing <code>@types/node</code> to
<code>with-svelte</code> example apps by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12585">vercel/turborepo#12585</a></li>
</ul>
<h3>Changelog</h3>
<ul>
<li>docs: Add Bun equivalent for updating dependencies by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12580">vercel/turborepo#12580</a></li>
<li>fix: Mention <code>turbo.json</code> in concurrency error message by
<a href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a>
in <a
href="https://redirect.github.com/vercel/turborepo/pull/12582">vercel/turborepo#12582</a></li>
<li>fix: Surface actionable message when remote cache is requested but
not linked by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12584">vercel/turborepo#12584</a></li>
<li>chore: Delete agents app by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12587">vercel/turborepo#12587</a></li>
<li>fix: Load custom CA certificates in fast webpki-only HTTP client by
<a href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a>
in <a
href="https://redirect.github.com/vercel/turborepo/pull/12591">vercel/turborepo#12591</a></li>
<li>docs: Remove pre-release badges by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12592">vercel/turborepo#12592</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/vercel/turborepo/compare/v2.9.5...v2.9.6">https://github.com/vercel/turborepo/compare/v2.9.5...v2.9.6</a></p>
<h2>Turborepo v2.9.6-canary.3</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Changelog</h3>
<ul>
<li>chore: Delete agents app by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12587">vercel/turborepo#12587</a></li>
<li>fix: Load custom CA certificates in fast webpki-only HTTP client by
<a href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a>
in <a
href="https://redirect.github.com/vercel/turborepo/pull/12591">vercel/turborepo#12591</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/vercel/turborepo/compare/v2.9.6-canary.2...v2.9.6-canary.3">https://github.com/vercel/turborepo/compare/v2.9.6-canary.2...v2.9.6-canary.3</a></p>
<h2>Turborepo v2.9.6-canary.2</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>create-turbo</h3>
<ul>
<li>chore: Update dependencies found in audits by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12586">vercel/turborepo#12586</a></li>
</ul>
<h3>Examples</h3>
<ul>
<li>fix: Add missing <code>@types/node</code> to
<code>with-svelte</code> example apps by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12585">vercel/turborepo#12585</a></li>
</ul>
<h3>Changelog</h3>
<ul>
<li>fix: Surface actionable message when remote cache is requested but
not linked by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12584">vercel/turborepo#12584</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/vercel/turborepo/compare/v2.9.6-canary.1...v2.9.6-canary.2">https://github.com/vercel/turborepo/compare/v2.9.6-canary.1...v2.9.6-canary.2</a></p>
<h2>Turborepo v2.9.6-canary.1</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3><code>@​turbo/telemetry</code></h3>
<ul>
<li>fix: Suppress telemetry alert when running on Vercel by <a
href="https://github.com/anthonyshew"><code>@​anthonyshew</code></a> in
<a
href="https://redirect.github.com/vercel/turborepo/pull/12576">vercel/turborepo#12576</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/turborepo/commit/77bca2be4ead656434f8556f8d8cf0ee8e43542d"><code>77bca2b</code></a>
publish 2.9.6 to registry</li>
<li><a
href="https://github.com/vercel/turborepo/commit/5a8f2e8606b08d5182839b3a671fe7fef58403c4"><code>5a8f2e8</code></a>
release(turborepo): 2.9.6-canary.3 (<a
href="https://redirect.github.com/vercel/turborepo/issues/12593">#12593</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/861efa8925c74673bf5875df88f514b34bfd1a58"><code>861efa8</code></a>
docs: Remove pre-release badges (<a
href="https://redirect.github.com/vercel/turborepo/issues/12592">#12592</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/28db7d051a63b8d06ce825644829a65269008115"><code>28db7d0</code></a>
fix: Load custom CA certificates in fast webpki-only HTTP client (<a
href="https://redirect.github.com/vercel/turborepo/issues/12591">#12591</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/b4121773823f746e587d9c7bba58e0aab98ae2a3"><code>b412177</code></a>
release(turborepo): 2.9.6-canary.2 (<a
href="https://redirect.github.com/vercel/turborepo/issues/12588">#12588</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/9018c65cce5f90d17c3cb46fb2ee78b91237490b"><code>9018c65</code></a>
chore: Delete agents app (<a
href="https://redirect.github.com/vercel/turborepo/issues/12587">#12587</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/5d19186bf3847626988ed4d984d0284c1b5f4401"><code>5d19186</code></a>
chore: Update dependencies found in audits (<a
href="https://redirect.github.com/vercel/turborepo/issues/12586">#12586</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/8338f427ddf6a92c87aa3fd413d45c6be042a37e"><code>8338f42</code></a>
fix: Add missing <code>@types/node</code> to <code>with-svelte</code>
example apps (<a
href="https://redirect.github.com/vercel/turborepo/issues/12585">#12585</a>)</li>
<li><a
href="https://github.com/vercel/turborepo/commit/219b602d24f9b5006e3adf8f86a70c724ae9df70"><code>219b602</code></a>
fix: Surface actionable message when remote cache is requested but not
linked...</li>
<li><a
href="https://github.com/vercel/turborepo/commit/aba98af5dab1b121e0215a09763b21501e58735d"><code>aba98af</code></a>
release(turborepo): 2.9.6-canary.1 (<a
href="https://redirect.github.com/vercel/turborepo/issues/12583">#12583</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/turborepo/compare/v2.9.3...v2.9.6">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:07:29 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> b2360c535a build(deps): Bump react-icons from 5.5.0 to 5.6.0 (#132)
Bumps [react-icons](https://github.com/react-icons/react-icons) from
5.5.0 to 5.6.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/react-icons/react-icons/releases">react-icons's
releases</a>.</em></p>
<blockquote>
<h2>v5.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Bump prismjs from 1.29.0 to 1.30.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1033">react-icons/react-icons#1033</a></li>
<li>Bump <code>@​babel/helpers</code> from 7.23.2 to 7.26.10 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1036">react-icons/react-icons#1036</a></li>
<li>Bump http-proxy-middleware from 2.0.7 to 2.0.9 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1041">react-icons/react-icons#1041</a></li>
<li>Bump form-data from 3.0.1 to 3.0.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1058">react-icons/react-icons#1058</a></li>
<li>Bump vite from 5.4.14 to 5.4.20 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1071">react-icons/react-icons#1071</a></li>
<li>upgrade packages by <a
href="https://github.com/kamijin-fanta"><code>@​kamijin-fanta</code></a>
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1075">react-icons/react-icons#1075</a></li>
<li>Bump tar from 6.2.0 to 6.2.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1078">react-icons/react-icons#1078</a></li>
<li>Bump vite from 6.3.6 to 6.4.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1081">react-icons/react-icons#1081</a></li>
<li>Bump tmp from 0.2.3 to 0.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1077">react-icons/react-icons#1077</a></li>
<li>Bump glob from 11.0.3 to 11.1.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1089">react-icons/react-icons#1089</a></li>
<li>Bump <code>@​babel/runtime</code> from 7.23.2 to 7.28.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1073">react-icons/react-icons#1073</a></li>
<li>Bump node-forge from 1.3.1 to 1.3.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1092">react-icons/react-icons#1092</a></li>
<li>Bump mdast-util-to-hast from 13.0.2 to 13.2.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1094">react-icons/react-icons#1094</a></li>
<li>Bump undici from 7.16.0 to 7.18.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1103">react-icons/react-icons#1103</a></li>
<li>Bump devalue from 5.3.2 to 5.6.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1104">react-icons/react-icons#1104</a></li>
<li>Bump h3 from 1.15.4 to 1.15.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1105">react-icons/react-icons#1105</a></li>
<li>Bump diff from 5.2.0 to 5.2.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1107">react-icons/react-icons#1107</a></li>
<li>Bump webpack from 5.94.0 to 5.104.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1110">react-icons/react-icons#1110</a></li>
<li>Bump jsonpath from 1.1.1 to 1.2.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1113">react-icons/react-icons#1113</a></li>
<li>Bump devalue from 5.6.2 to 5.6.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1115">react-icons/react-icons#1115</a></li>
<li>Bump astro from 5.14.1 to 5.15.9 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1091">react-icons/react-icons#1091</a></li>
<li>Bump ajv from 6.12.6 to 6.14.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/react-icons/react-icons/pull/1116">react-icons/react-icons#1116</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/react-icons/react-icons/compare/v5.5.0...v5.6.0">https://github.com/react-icons/react-icons/compare/v5.5.0...v5.6.0</a></p>
<table>
<thead>
<tr>
<th>Icon Library</th>
<th>License</th>
<th>Version</th>
<th align="right">Count</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://circumicons.com/">Circum Icons</a></td>
<td><a
href="https://github.com/Klarr-Agency/Circum-Icons/blob/main/LICENSE">MPL-2.0
license</a></td>
<td>1.0.0</td>
<td align="right">288</td>
</tr>
<tr>
<td><a href="https://fontawesome.com/">Font Awesome 5</a></td>
<td><a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0
License</a></td>
<td>5.15.4-3-gafecf2a</td>
<td align="right">1612</td>
</tr>
<tr>
<td><a href="https://fontawesome.com/">Font Awesome 6</a></td>
<td><a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0
License</a></td>
<td>6.7.2-1-g840c215</td>
<td align="right">2060</td>
</tr>
<tr>
<td><a href="https://ionicons.com/">Ionicons 4</a></td>
<td><a
href="https://github.com/ionic-team/ionicons/blob/master/LICENSE">MIT</a></td>
<td>4.6.3</td>
<td align="right">696</td>
</tr>
<tr>
<td><a href="https://ionicons.com/">Ionicons 5</a></td>
<td><a
href="https://github.com/ionic-team/ionicons/blob/master/LICENSE">MIT</a></td>
<td>5.5.4</td>
<td align="right">1332</td>
</tr>
<tr>
<td><a href="http://google.github.io/material-design-icons/">Material
Design icons</a></td>
<td><a
href="https://github.com/google/material-design-icons/blob/master/LICENSE">Apache
License Version 2.0</a></td>
<td>4.0.0-142-gbb04090f93</td>
<td align="right">4341</td>
</tr>
<tr>
<td><a href="http://s-ings.com/typicons/">Typicons</a></td>
<td><a href="https://creativecommons.org/licenses/by-sa/3.0/">CC BY-SA
3.0</a></td>
<td>2.1.2</td>
<td align="right">336</td>
</tr>
<tr>
<td><a href="https://octicons.github.com/">Github Octicons
icons</a></td>
<td><a
href="https://github.com/primer/octicons/blob/master/LICENSE">MIT</a></td>
<td>18.3.0</td>
<td align="right">264</td>
</tr>
<tr>
<td><a href="https://feathericons.com/">Feather</a></td>
<td><a
href="https://github.com/feathericons/feather/blob/master/LICENSE">MIT</a></td>
<td>4.29.2</td>
<td align="right">287</td>
</tr>
<tr>
<td><a href="https://lucide.dev/">Lucide</a></td>
<td><a
href="https://github.com/lucide-icons/lucide/blob/main/LICENSE">ISC</a></td>
<td>0.462.0</td>
<td align="right">1541</td>
</tr>
<tr>
<td><a href="https://game-icons.net/">Game Icons</a></td>
<td><a href="https://creativecommons.org/licenses/by/3.0/">CC BY
3.0</a></td>
<td>12920d6565588f0512542a3cb0cdfd36a497f910</td>
<td align="right">4040</td>
</tr>
<tr>
<td><a href="https://erikflowers.github.io/weather-icons/">Weather
Icons</a></td>
<td><a href="http://scripts.sil.org/OFL">SIL OFL 1.1</a></td>
<td>2.0.12</td>
<td align="right">219</td>
</tr>
<tr>
<td><a href="https://vorillaz.github.io/devicons/">Devicons</a></td>
<td><a href="https://opensource.org/licenses/MIT">MIT</a></td>
<td>1.8.0</td>
<td align="right">192</td>
</tr>
<tr>
<td><a href="https://github.com/ant-design/ant-design-icons">Ant Design
Icons</a></td>
<td><a href="https://opensource.org/licenses/MIT">MIT</a></td>
<td>4.4.2</td>
<td align="right">831</td>
</tr>
<tr>
<td><a href="https://github.com/twbs/icons">Bootstrap Icons</a></td>
<td><a href="https://opensource.org/licenses/MIT">MIT</a></td>
<td>1.13.1</td>
<td align="right">2754</td>
</tr>
<tr>
<td><a href="https://github.com/Remix-Design/RemixIcon">Remix
Icon</a></td>
<td><a href="http://www.apache.org/licenses/">Apache License Version
2.0</a></td>
<td>4.6.0</td>
<td align="right">3058</td>
</tr>
<tr>
<td><a href="https://github.com/icons8/flat-color-icons">Flat Color
Icons</a></td>
<td><a href="https://opensource.org/licenses/MIT">MIT</a></td>
<td>1.0.2</td>
<td align="right">329</td>
</tr>
<tr>
<td><a
href="https://github.com/grommet/grommet-icons">Grommet-Icons</a></td>
<td><a href="http://www.apache.org/licenses/">Apache License Version
2.0</a></td>
<td>4.14.0</td>
<td align="right">637</td>
</tr>
<tr>
<td><a
href="https://github.com/tailwindlabs/heroicons">Heroicons</a></td>
<td><a href="https://opensource.org/licenses/MIT">MIT</a></td>
<td>1.0.6</td>
<td align="right">460</td>
</tr>
<tr>
<td><a href="https://github.com/tailwindlabs/heroicons">Heroicons
2</a></td>
<td><a href="https://opensource.org/licenses/MIT">MIT</a></td>
<td>2.2.0</td>
<td align="right">972</td>
</tr>
</tbody>
</table>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/react-icons/react-icons/commit/6501a4156153b396d062deffed5ed2c8eb64a458"><code>6501a41</code></a>
v5.6.0</li>
<li><a
href="https://github.com/react-icons/react-icons/commit/387e78027eeea4ef28df13b732e5906725ee3c2f"><code>387e780</code></a>
update icons</li>
<li><a
href="https://github.com/react-icons/react-icons/commit/fb057e730c8f3bfb9b698ecf7f3f92ee3c43afd9"><code>fb057e7</code></a>
5.5.1-snapshot.0</li>
<li><a
href="https://github.com/react-icons/react-icons/commit/6f7475550af2cacfcfa87e4127b8803a3d5fee6c"><code>6f74755</code></a>
update eslint</li>
<li><a
href="https://github.com/react-icons/react-icons/commit/73c281f3323d720a5cf8ff626ce4dd04e62afb88"><code>73c281f</code></a>
Bump ajv from 6.12.6 to 6.14.0 (<a
href="https://redirect.github.com/react-icons/react-icons/issues/1116">#1116</a>)</li>
<li><a
href="https://github.com/react-icons/react-icons/commit/3f2b26446f3943a2a11c083f2073e20c3f222c7b"><code>3f2b264</code></a>
Bump astro from 5.14.1 to 5.15.9 (<a
href="https://redirect.github.com/react-icons/react-icons/issues/1091">#1091</a>)</li>
<li><a
href="https://github.com/react-icons/react-icons/commit/04adc762186d4adc6428279cc6d9119f2894ae20"><code>04adc76</code></a>
Bump devalue from 5.6.2 to 5.6.3 (<a
href="https://redirect.github.com/react-icons/react-icons/issues/1115">#1115</a>)</li>
<li><a
href="https://github.com/react-icons/react-icons/commit/44a5e85bcc53a51950cb53f25f9f5ed8fdbdafb8"><code>44a5e85</code></a>
Bump jsonpath from 1.1.1 to 1.2.1 (<a
href="https://redirect.github.com/react-icons/react-icons/issues/1113">#1113</a>)</li>
<li><a
href="https://github.com/react-icons/react-icons/commit/e2c1d6ccc98ab477b7aa992bf1f9836b6eca80da"><code>e2c1d6c</code></a>
Bump webpack from 5.94.0 to 5.104.1 (<a
href="https://redirect.github.com/react-icons/react-icons/issues/1110">#1110</a>)</li>
<li><a
href="https://github.com/react-icons/react-icons/commit/f3dca028cec5bb6012b95dc03e3ff0efb71709b5"><code>f3dca02</code></a>
Bump diff from 5.2.0 to 5.2.2 (<a
href="https://redirect.github.com/react-icons/react-icons/issues/1107">#1107</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/react-icons/react-icons/compare/v5.5.0...v5.6.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:05:39 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a552a04942 build(deps-dev): Bump tailwindcss from 4.2.1 to 4.2.2 (#128)
Bumps
[tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss)
from 4.2.1 to 4.2.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tailwindlabs/tailwindcss/releases">tailwindcss's
releases</a>.</em></p>
<blockquote>
<h2>v4.2.2</h2>
<h3>Added</h3>
<ul>
<li>Support Vite 8 in <code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19790">#19790</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Don't crash when candidates contain prototype properties like
<code>row-constructor</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19725">#19725</a>)</li>
<li>Canonicalize <code>calc(var(--spacing)*…)</code> expressions into
<code>--spacing(…)</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19769">#19769</a>)</li>
<li>Fix crash in canonicalization step when handling utilities
containing <code>@property</code> at-rules (e.g. <code>shadow-sm
border</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19727">#19727</a>)</li>
<li>Skip full reload for server only modules scanned by client CSS when
using <code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19745">#19745</a>)</li>
<li>Improve canonicalization for bare values exceeding default spacing
scale suggestions (e.g. <code>w-1234 h-1234</code> →
<code>size-1234</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19809">#19809</a>)</li>
<li>Fix canonicalization resulting in empty list (e.g. <code>w-5 h-5
size-5</code> → <code>''</code> instead of <code>size-5</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19812">#19812</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md">tailwindcss's
changelog</a>.</em></p>
<blockquote>
<h2>[4.2.2] - 2026-03-18</h2>
<h3>Fixed</h3>
<ul>
<li>Don't crash when candidates contain prototype properties like
<code>row-constructor</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19725">#19725</a>)</li>
<li>Canonicalize <code>calc(var(--spacing)*…)</code> expressions into
<code>--spacing(…)</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19769">#19769</a>)</li>
<li>Fix crash in canonicalization step when handling utilities
containing <code>@property</code> at-rules (e.g. <code>shadow-sm
border</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19727">#19727</a>)</li>
<li>Skip full reload for server only modules scanned by client CSS when
using <code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19745">#19745</a>)</li>
<li>Add support for Vite 8 in <code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19790">#19790</a>)</li>
<li>Improve canonicalization for bare values exceeding default spacing
scale suggestions (e.g. <code>w-1234 h-1234</code> →
<code>size-1234</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19809">#19809</a>)</li>
<li>Fix canonicalization resulting in empty list (e.g. <code>w-5 h-5
size-5</code> → <code>''</code> instead of <code>size-5</code>) (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19812">#19812</a>)</li>
<li>Resolve tsconfig paths to allow for <code>@import
'@/path/to/file';</code> when using <code>@tailwindcss/vite</code> (<a
href="https://redirect.github.com/tailwindlabs/tailwindcss/pull/19803">#19803</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/d596b0c43d36ad5099c983930fb155e089cbc291"><code>d596b0c</code></a>
4.2.2 (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss/issues/19821">#19821</a>)</li>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/2228a57a9e6a5519f390da3f3a323a2cdfd5312f"><code>2228a57</code></a>
Bump Lightning CSS (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss/issues/19771">#19771</a>)</li>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/f302fce815786dedbb97baf81f666d9a28c55a24"><code>f302fce</code></a>
Fix canonicalization resulting in empty list (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss/issues/19812">#19812</a>)</li>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/bb2f1705142cc0563e44113e3aef604e7c860c0e"><code>bb2f170</code></a>
Improve canonicalization for bare values exceeding default spacing scale
sugg...</li>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/faa5e8849be45e1009dc1d0f862ca782ee0a1ee9"><code>faa5e88</code></a>
Cleanup inconsistencies related to (regex) escapes (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss/issues/19804">#19804</a>)</li>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/d5717f2307d61ff2d5531326e5fbf36f9ad6dabc"><code>d5717f2</code></a>
run prettier</li>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/51aa9d799c59f79723ba032ed11c8010c4d490a3"><code>51aa9d7</code></a>
fix(canonicalize): handle utilities with empty property maps in collapse
(<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss/issues/19">#19</a>...</li>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/c586bd6a94a5415e7154474980d1c664e28b6991"><code>c586bd6</code></a>
Canonicalize <code>calc(var(--spacing)*…)</code> expressions into
<code>--spacing(…)</code> (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss/issues/19769">#19769</a>)</li>
<li><a
href="https://github.com/tailwindlabs/tailwindcss/commit/9ded4a23de06fb7e8cdc34a3bdf9318e7e8d2bbc"><code>9ded4a2</code></a>
Guard object lookups against inherited prototype properties (<a
href="https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss/issues/19725">#19725</a>)</li>
<li>See full diff in <a
href="https://github.com/tailwindlabs/tailwindcss/commits/v4.2.2/packages/tailwindcss">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:04:12 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 775abe704c build(deps-dev): Bump knip from 6.2.0 to 6.4.1 (#164)
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip)
from 6.2.0 to 6.4.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/webpro-nl/knip/releases">knip's
releases</a>.</em></p>
<blockquote>
<h2>Release 6.4.1</h2>
<ul>
<li>license (2d3d8d86ea51f18224c3558a38c28df00113f683)</li>
<li>Handle file path arguments in Bun plugin (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1678">#1678</a>)
(cdbe298efcb226ae4baec6567a0cbb6fdf001ee8)</li>
</ul>
<h2>Release 6.4.0</h2>
<ul>
<li>chore: change license file to have more conventional casing (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1664">#1664</a>)
(ed97e6a37) - thanks <a
href="https://github.com/Zamiell"><code>@​Zamiell</code></a>!</li>
<li>fix: whitelist spelling error (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1666">#1666</a>)
(bcad12012) - thanks <a
href="https://github.com/Zamiell"><code>@​Zamiell</code></a>!</li>
<li>Add Panda CSS plugin (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1671">#1671</a>)
(7ab0d4d88) - thanks <a
href="https://github.com/Faithfinder"><code>@​Faithfinder</code></a>!</li>
<li>fix(rspack): detect plugins from swc-loader (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1675">#1675</a>)
(1431ff3d5) - thanks <a
href="https://github.com/TkDodo"><code>@​TkDodo</code></a>!</li>
<li>docs: fix broken anchor link in DEVELOPMENT.md (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1677">#1677</a>)
(e78c7f609) - thanks <a
href="https://github.com/vincent067"><code>@​vincent067</code></a>!</li>
<li>Fix <code>@jest-environment</code> pragma resolution and limit
pragma scope (3832364e2)</li>
<li>Track Object enumeration to skip enum-member checks in nsTypes mode
(a47aff535)</li>
<li>Cache module resolution by directory + specifier (cefed38c2)</li>
<li>Skip read+parse in walkAndAnalyze when the file cache will hit
(25a7982c1)</li>
<li>Collapse double statSync in FileEntryCache.reconcile
(b63fbd9f2)</li>
<li>Refactor walkAndAnalyze: dedupe cached/uncached paths
(573df54ba)</li>
<li>Cache glob results across <code>--cache</code> runs (6ab8de805)</li>
<li>Mark namespace members used when enumerated via Object.*
(dabf8ce77)</li>
<li>Add config hints for redundant/unregistered extensions (close <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1672">#1672</a>,
close <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1673">#1673</a>)
(050689575)</li>
<li>Enable Tailwind CSS compiler for <a
href="https://github.com/tailwindcss"><code>@​tailwindcss</code></a>
integrations (close <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1674">#1674</a>)
(f3ed14d0e)</li>
<li>Resolve scss path aliases (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1676">#1676</a>)
(00ae83be9)</li>
<li>Re-gen plugins.md (35d8fabaa)</li>
<li>Resolve path.join(__dirname, ...) in Worker/child_process calls (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1660">#1660</a>)
(40a917e3f)</li>
<li>license (608f0ed76)</li>
</ul>
<h2>Release 6.3.1</h2>
<ul>
<li>Fix peer dependency resolution (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1645">#1645</a>)
(0a6e93d16f89bde270a5458c0e76e1c6a996012f) - thanks <a
href="https://github.com/controversial"><code>@​controversial</code></a>!</li>
<li>Support absolute file paths in react-router routes (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1659">#1659</a>)
(97bb283e31eb72da8c60fcede607b22da2929103) - thanks <a
href="https://github.com/mpalmer685"><code>@​mpalmer685</code></a>!</li>
<li>Track namespace imports used as object property values (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1669">#1669</a>)
(c4d6b28df5955abbe9dde40d245ceb5a6e0bc669)</li>
<li>Split plugins/util smoke globs to fit Windows cmd.exe limit
(33ef87d15770d0ee9dc248a640538ce2f0d75a46)</li>
</ul>
<h2>Release 6.3.0</h2>
<ul>
<li>Re-gen sponsorships chart
(83ee4895f36d934bf9f2efaaaf3e141c33c889f8)</li>
<li>Add testimonial (be16c54d379698899e3f10646bb23b280024b989)</li>
<li>Add deferred resolve entries as direct entry paths to avoid ws
exclusion (91a183539fb63528f900b187328c3bba1b161e88)</li>
<li>Add rolldown plugin (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1661">#1661</a>)
(44bfd6273375384dc0b01b2cf673b6ad1b6783f0)</li>
<li>Housekeep plugins a bit
(dc6986821bde185185b730e119d2c528048f9976)</li>
<li>Improve <code>module.register</code> handling
(b9e36ea42d0b2b35d6080aafc94b292a3d7fd711)</li>
<li>Housekeep release script
(f81bd0f5f6630c2cac9934c3b96ace0d42bd2353)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/webpro-nl/knip/commit/ea70d72232b13dca6338105f9fd4ea211595dfba"><code>ea70d72</code></a>
Release knip@6.4.1</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/cdbe298efcb226ae4baec6567a0cbb6fdf001ee8"><code>cdbe298</code></a>
Handle file path arguments in Bun plugin (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1678">#1678</a>)</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/baa8ef4e1fe035cde0db3bcf04f9f11171e4e589"><code>baa8ef4</code></a>
Release knip@6.4.0</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/608f0ed765e1a950e54f7d618ce37c7977d8b5cf"><code>608f0ed</code></a>
license</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/40a917e3f412b322d68e1bd78645f86196934d83"><code>40a917e</code></a>
Resolve path.join(__dirname, ...) in Worker/child_process calls (<a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1660">#1660</a>)</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/00ae83be9a8b80589448bbf2defbb27ad3334add"><code>00ae83b</code></a>
Resolve scss path aliases (resolve <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1676">#1676</a>)</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/f3ed14d0ec4c3a7ec46ed859a88b72538b550582"><code>f3ed14d</code></a>
Enable Tailwind CSS compiler for <a
href="https://github.com/tailwindcss"><code>@​tailwindcss</code></a>
integrations (close <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1674">#1674</a>)</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/050689575eeb47f31874dbd3ac3c689b54c0551b"><code>0506895</code></a>
Add config hints for redundant/unregistered extensions (close <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1672">#1672</a>,
close <a
href="https://github.com/webpro-nl/knip/tree/HEAD/packages/knip/issues/1">#1</a>...</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/dabf8ce77676d60534d9e002824f08854ffd4440"><code>dabf8ce</code></a>
Mark namespace members used when enumerated via Object.*</li>
<li><a
href="https://github.com/webpro-nl/knip/commit/6ab8de8057437637b142ce25d7001d8d279d8603"><code>6ab8de8</code></a>
Cache glob results across <code>--cache</code> runs</li>
<li>Additional commits viewable in <a
href="https://github.com/webpro-nl/knip/commits/knip@6.4.1/packages/knip">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 18:01:15 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fca39d25a8 build(deps-dev): Bump globals from 17.4.0 to 17.5.0 (#163)
Bumps [globals](https://github.com/sindresorhus/globals) from 17.4.0 to
17.5.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sindresorhus/globals/releases">globals's
releases</a>.</em></p>
<blockquote>
<h2>v17.5.0</h2>
<ul>
<li>Update globals (2026-04-12) (<a
href="https://redirect.github.com/sindresorhus/globals/issues/342">#342</a>)
5d84602</li>
</ul>
<hr />
<p><a
href="https://github.com/sindresorhus/globals/compare/v17.4.0...v17.5.0">https://github.com/sindresorhus/globals/compare/v17.4.0...v17.5.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sindresorhus/globals/commit/b8170c8e1d648291b613c5b39a69652c796fa36c"><code>b8170c8</code></a>
17.5.0</li>
<li><a
href="https://github.com/sindresorhus/globals/commit/5d846029679832931f38ced6381cc95bcb9abd80"><code>5d84602</code></a>
Update globals (2026-04-12) (<a
href="https://redirect.github.com/sindresorhus/globals/issues/342">#342</a>)</li>
<li><a
href="https://github.com/sindresorhus/globals/commit/1b727e5f4cc39121b8e77b9f27574a8ca27391fc"><code>1b727e5</code></a>
Fix build script for ES globals (<a
href="https://redirect.github.com/sindresorhus/globals/issues/341">#341</a>)</li>
<li>See full diff in <a
href="https://github.com/sindresorhus/globals/compare/v17.4.0...v17.5.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=globals&package-manager=npm_and_yarn&previous-version=17.4.0&new-version=17.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 17:54:42 +02:00
martin-mfg dbe6aa0033 fix playwright test 2026-04-20 17:09:44 +02:00
martin-mfg b40c4b7915 move env from constants to SvgInline, cleanup 2026-04-20 16:39:17 +02:00
martin-mfg f7b1b9f382 frontend: try loading env explicitly 2026-04-19 20:16:42 +02:00
martin-mfg 5ccb799198 extend debug log 2026-04-19 19:34:02 +02:00
martin-mfg 00f5ac900e Merge remote-tracking branch 'origin/master' 2026-04-19 19:22:14 +02:00
martin-mfg 3b67bd6bcb add debug logs 2026-04-19 19:21:25 +02:00
martin-mfg e58bee0bfd add debug logs 2026-04-19 18:57:55 +02:00
Martin f346dcaa73 small frontend fixes (#162) 2026-04-19 18:36:22 +02:00
Marco Pasqualetti fc4ee7507b test: remove stdout and std from test by mocking logger module (#161)
- Same as #143 
- Resolves
https://github.com/stats-organization/github-stats-extended/pull/143#discussion_r3105155487
- Closes #141
2026-04-19 09:28:46 +02:00
MartinandMarco Pasqualetti d476f8cbe1 extract "core" package, small improvements (#137)
This PR mainly refactors the code by extracting a "core" package which
is then used by the "backend" and "frontend" apps.

Apart from this the PR also contains several smaller changes like fixing
dependabot, upgrading dependencies, aligned "package.json" files, added
tests, ...

addresses step 1 of #32 
closes #136

---------

Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2026-04-18 10:37:26 +02:00
martin-mfg 19c39f02af document default regeneration times 2026-03-08 09:42:09 +01:00
324 changed files with 26359 additions and 14520 deletions
+1 -1
View File
@@ -21,4 +21,4 @@ If you believe someone is violating the code of conduct, we ask that you report
- Repeated harassment of others. In general, if someone asks you to stop, then stop.
- **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. Remember that were different. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesnt mean that theyre wrong. Dont forget that it is human to err and blaming each other doesnt get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
Based on the [Django Code of Conduct](https://www.djangoproject.com/conduct/).
Based on the [Django Code of Conduct](https://www.djangoproject.com/conduct/).
+52 -23
View File
@@ -5,12 +5,59 @@
To set up the project GitHub-Stats-Extended locally, run the following commands:
```bash
./vercel-preparation.sh
pnpm install
pnpm --filter frontend run build
pnpm run build:packages
pnpm run dev:frontend
```
The easiest way to run and test the project is to deploy it to Vercel as described in the [deployment guide](../docs/deploy.md).
### Backend server
The wizard renders its previews in the browser, but the docs reference cards by
root-relative path (`/api?username=...`), so those images only load with the backend running:
```bash
pnpm run dev:backend # card endpoints on :9000, proxied by the frontend dev server
```
It needs a [Personal Access Token](../docs/deploy.md#first-step-get-your-personal-access-token-pat) in `apps/backend/.env` (the SQL database is optional):
```
PAT_1=your_token_here
```
You can also deploy to Vercel and test there, as described in the [deployment guide](../docs/deploy.md).
## Tests
```bash
pnpm run test # unit tests
pnpm run lint # eslint
pnpm run typecheck # tsc
```
The **Backend E2E test** in CI compares the cards your branch renders against the ones served by the preview deployment, which is still on the last commit merged to master.
So if your PR changes card output at all, that job goes red until the preview catches up.
It's marked `continue-on-error`, so it won't block your PR, but do open it and check the diff is only what you expected.
If the change to card markup was intentional, update the snapshots:
```bash
pnpm --filter ./packages/core/ run test:update:snapshot
pnpm --filter ./apps/backend/ run test:update:snapshot
```
## GraphQL Queries
The GraphQL queries live in `packages/core/src/graphql/queries/*.graphql`,
and their TypeScript types are generated from GitHub's published schema into `packages/core/src/graphql/generated/`.
Those generated files are committed, so if you change a query, regenerate them and include the result in your PR:
```bash
pnpm --filter ./packages/core/ run generate-graphql-types
```
CI runs `pnpm --filter ./packages/core/ run check-graphql-types`, which fails if the committed types no longer match the queries.
Never edit the generated files by hand — change the `.graphql` file and regenerate.
## Themes Contribution
@@ -20,7 +67,7 @@ We have stopped the addition of new themes to decrease maintenance efforts. If y
GitHub-Stats-Extended supports multiple languages. If we are missing your language, you can contribute it! You can check the currently supported languages [here](../docs/advanced_documentation.md#available-locales).
To contribute your language you need to edit the [backend/src/translations.js](../backend/src/translations.js) file and add a new property to each object where the key is the language code in [ISO 639-1 standard](https://www.andiamo.co.uk/resources/iso-language-codes/) and the value is the translated string.
To contribute your language you need to edit the [packages/core/src/translations.ts](../packages/core/src/translations.ts) file and add a new property to each object where the key is the language code in [ISO 639-1 standard](https://www.andiamo.co.uk/resources/iso-language-codes/) and the value is the translated string.
## Any contributions you make will be under the MIT Software License
@@ -30,25 +77,7 @@ In short, when you submit changes, your submissions are understood to be under t
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/stats-organization/github-stats-extended/issues/new/choose). If there is already an open issue for your bug in the upstream repo [github-readme-stats](https://github.com/anuraghazra/github-readme-stats/issues) you don't need to report it here.
## Frequently Asked Questions (FAQs)
**Q:** How to hide Jupyter Notebook?
> **Ans:** &hide=jupyter%20notebook
**Q:** Language Card is incorrect
> **Ans:** Please read all the related issues/comments before opening any issues regarding language card stats:
>
> - <https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665164174>
>
> - <https://github.com/anuraghazra/github-readme-stats/issues/136#issuecomment-665172181>
**Q:** How to count private stats?
> see [here](../docs/fork.md#private-contributions-support)
### Feature Request
## Feature Request
**Great Feature Requests** tend to have:
+1 -1
View File
@@ -1,3 +1,3 @@
# Security Policy
Please report any security vulnerabilities to github.stats.extended@gmail.com. I will try to respond as quickly as possible, but since I am maintaining this project in my free time, please allow some time for a response.
Please report any security vulnerabilities to github.stats.extended@gmail.com. I will try to respond as quickly as possible, but since I am maintaining this project in my free time, please allow some time for a response.
@@ -14,11 +14,11 @@ runs:
steps:
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Setup Node.js (via input)
if: ${{ inputs.node-version }}
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ inputs.node-version }}
cache: "pnpm"
@@ -26,7 +26,7 @@ runs:
- name: Setup Node.js (via .nvmrc)
if: ${{ !inputs.node-version }}
uses: actions/setup-node@v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
cache: "pnpm"
+54 -8
View File
@@ -5,8 +5,8 @@ updates:
directory: "/"
schedule:
interval: cron
cronjob: every day at 5am
open-pull-requests-limit: 10
cronjob: "45 0 * * 3,6"
open-pull-requests-limit: 20
commit-message:
prefix: "build(deps)"
prefix-development: "build(deps-dev)"
@@ -15,14 +15,61 @@ updates:
ignore:
- dependency-name: "@types/node"
update-types: ["version-update:semver-major"]
groups:
react:
patterns:
- "react"
- "react-dom"
- "react-*"
- "@types/react"
- "@types/react-dom"
redux:
patterns:
- "redux"
- "@reduxjs/*"
- "react-redux"
vite:
patterns:
- "vite"
- "@vitejs/*"
tailwind:
patterns:
- "tailwindcss"
- "@tailwindcss/*"
- "daisyui"
vitest:
patterns:
- "vitest"
- "@vitest/*"
eslint:
patterns:
- "eslint"
- "@eslint/*"
- "eslint-*"
- "typescript-eslint"
typescript:
patterns:
- "typescript"
- "@types/*"
playwright:
patterns:
- "@playwright/*"
axios:
patterns:
- "axios"
- "axios-*"
graphql:
patterns:
- "graphql"
- "@graphql-codegen/*"
- "@octokit/graphql-schema"
# Maintain dependencies for GitHub Actions
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: cron
cronjob: every day at 5am
open-pull-requests-limit: 10
interval: daily
open-pull-requests-limit: 20
commit-message:
prefix: "ci(deps)"
prefix-development: "ci(deps-dev)"
@@ -33,9 +80,8 @@ updates:
- package-ecosystem: devcontainers
directory: "/"
schedule:
interval: cron
cronjob: every day at 5am
open-pull-requests-limit: 10
interval: daily
open-pull-requests-limit: 20
commit-message:
prefix: "build(deps)"
prefix-development: "build(deps-dev)"
+48 -30
View File
@@ -31,26 +31,21 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Run vercel-preparation.sh
run: |
chmod +x ./vercel-preparation.sh
./vercel-preparation.sh
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
with:
node-version: ${{ matrix.node }}
- name: Build packages
run: pnpm run build:packages
- name: Build frontend
run: pnpm --filter frontend run build
run: pnpm run build:frontend
- name: Run frontend tests
run: pnpm --filter frontend run test
- name: Run backend tests
run: pnpm --filter github-readme-stats run test
- name: Run tests
run: pnpm run test
frontend-test-e2e:
name: Frontend E2E test
@@ -64,21 +59,48 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
- name: Run vercel-preparation.sh
run: |
chmod +x ./vercel-preparation.sh
./vercel-preparation.sh
- name: Build packages
run: pnpm run build:packages
# Only install chromium since it is the only browser enabled in
# apps/frontend/playwright.config.ts. Update this if more projects are added.
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
run: pnpm exec playwright install --with-deps chromium
- name: Run Playwright tests
run: pnpm --filter frontend run test:e2e
run: pnpm --filter ./apps/frontend/ run test:e2e
backend-test-e2e:
name: Backend E2E test
runs-on: ubuntu-latest
permissions:
contents: read
# Compares the cards we render here against the preview deployment,
# which is still on the last commit merged on master.
# So any PR that changes card output turns this red until the preview catches up.
# Worth a look, just not always a bug.
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
- name: Build packages
run: pnpm run build:packages
- name: Run backend end-to-end tests
run: pnpm --filter ./apps/backend/ run test:e2e
code-checks:
name: Code checks
@@ -90,17 +112,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
# Heads up!
#
# 1. Execution of this script is needed to resolve `.vercel` folder from `apps/frontend/src/components/Card/SVG.js`
# 2. This scripts removes `apps/backend/node_modules` breaking ESLints module resolution.
# Dependency installation must occur after running ./vercel-preparation.sh.
- name: Run vercel-preparation.sh
run: |
chmod +x ./vercel-preparation.sh
./vercel-preparation.sh
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
@@ -114,5 +126,11 @@ jobs:
- name: Lint (knip)
run: pnpm run lint:knip
- name: Lint (deduped deps)
run: pnpm run lint:deps
- name: Lint (generated GraphQL types)
run: pnpm --filter ./packages/core/ run check-graphql-types
- name: Typecheck
run: pnpm run typecheck
@@ -11,50 +11,37 @@ on:
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: "0 0 */30 * *"
- cron: "45 22 */7 * *"
permissions:
actions: read
checks: read
contents: write
deployments: read
issues: read
discussions: read
packages: read
pages: read
pull-requests: write
repository-projects: read
security-events: read
statuses: read
jobs:
update-languages:
if: |
github.repository == 'anuraghazra/github-readme-stats' ||
github.repository == 'stats-organization/github-readme-stats' ||
github.repository == 'stats-organization/github-stats-extended'
name: Update supported languages
if: github.repository == 'stats-organization/github-stats-extended'
name: Generate Language Colors
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
- name: Run update-languages-json.js script
run: pnpm --filter github-readme-stats run generate-langs-json
run: pnpm --filter ./packages/core/ run generate-language-colors
- name: Create Pull Request if upstream language file is changed
uses: peter-evans/create-pull-request@v8
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "feat(backend): update languages JSON"
commit-message: "feat(core): update language colors"
branch: "update_langs/patch"
delete-branch: true
title: Update languages JSON
body: "The
[update-langs](https://github.com/stats-organization/github-stats-extended/actions/workflows/update-langs.yml)
action found new/updated languages in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)."
title: Update languages colors
body: >-
The [generate-language-colors](https://github.com/stats-organization/github-stats-extended/actions/workflows/generate-language-colors.yml)
workflow detected changes in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml) and regenerated the local language colors JSON file.
labels: "ci"
-53
View File
@@ -1,53 +0,0 @@
name: Generate Theme Readme
on:
push:
branches:
- master
paths:
- "apps/backend/themes/index.js"
workflow_dispatch:
permissions: {}
jobs:
generate-theme-doc:
runs-on: ubuntu-latest
name: Generate theme doc
permissions:
actions: read
checks: read
contents: write
deployments: read
issues: read
discussions: read
packages: read
pages: read
pull-requests: write
repository-projects: read
security-events: read
statuses: read
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
- name: Generate readme
run: |
pnpm --filter github-readme-stats run theme-readme-gen
- name: Create Pull Request if themes README has changed
uses: peter-evans/create-pull-request@v8
with:
commit-message: "feat(backend): update themes README"
branch: "update_themes_readme/patch"
delete-branch: true
title: Update themes README
body: "The
[generate-theme-doc](https://github.com/stats-organization/github-stats-extended/actions/workflows/generate-theme-doc.yml)
action found new/updated languages in the [upstream languages JSON
file](https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml)."
labels: "ci"
@@ -0,0 +1,54 @@
name: Generate Theme Readme
on:
push:
branches:
- master
paths:
- "packages/core/src/themes/index.ts"
workflow_dispatch:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# │ │ │ │ │
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: "45 22 */7 * *"
permissions: {}
jobs:
generate-theme-doc:
runs-on: ubuntu-latest
name: Generate Theme Readme
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
- name: Generate readme
run: pnpm run generate-theme-readme
- name: Create Pull Request if themes README has changed
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "feat(core): update themes README"
branch: "update_themes_readme/patch"
delete-branch: true
title: Update themes README
body: >-
The [generate-theme-readme](https://github.com/stats-organization/github-stats-extended/actions/workflows/generate-theme-readme.yml)
workflow detected changes in
[`packages/core/src/themes/index.ts`](https://github.com/stats-organization/github-stats-extended/blob/master/packages/core/src/themes/index.ts)
and regenerated the themes README.
labels: "ci"
+30
View File
@@ -0,0 +1,30 @@
name: Publish core package to npm
on:
workflow_dispatch:
release:
types: [released]
permissions: {}
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Dependencies
uses: ./.github/actions/install-dependencies
- name: Verify core package
run: |
pnpm --filter @stats-organization/github-readme-stats-core run typecheck
pnpm test
pnpm build:packages
pnpm --filter @stats-organization/github-readme-stats-core pack --dry-run
- name: Publish core package to npm
run: pnpm --filter @stats-organization/github-readme-stats-core publish --provenance --access public --no-git-checks
+18 -11
View File
@@ -10,22 +10,29 @@ on:
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
- cron: "45 * * * *"
- cron: "15,45 * * * *"
workflow_dispatch:
permissions: {}
jobs:
triggerRepeatRecent:
repeat-recent-requests:
if: github.repository == 'stats-organization/github-stats-extended'
name: Trigger server to update cached data.
runs-on: ubuntu-latest
steps:
- name: Make Request
id: myRequest
uses: fjogeleit/http-request-action@v2
with:
url: "https://github-stats-extended.vercel.app/api/repeat-recent"
method: "POST"
timeout: "300000"
- name: Show Response
run: |
echo ${{ steps.myRequest.outputs.response }}
echo ${{ steps.myRequest.outputs.status }}
args=(
--silent --show-error # no progress bar, but still report errors
--request POST # the only thing the endpoint checks
--location # a redirect would otherwise pass the status check
--max-redirs 5 # bounded well below curl's default of 50
--speed-limit 1 --speed-time 300 # give up after 300s of no traffic
--output response.txt # keep the body out of the status capture
--write-out "%{http_code}" # status is all we capture on stdout
)
status=$(curl "${args[@]}" "https://github-stats-extended.vercel.app/api/repeat-recent")
cat response.txt
printf '\nstatus: %s\n' "$status"
test "$status" -lt 400
+18
View File
@@ -0,0 +1,18 @@
name: Update version tags
on:
push:
branches-ignore:
- "**"
tags:
- "v*.*.*"
permissions: {}
jobs:
update-semver:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: haya14busa/action-update-semver@7d2c558640ea49e798d46539536190aff8c18715 # v1.5.1
+6 -14
View File
@@ -1,18 +1,8 @@
node_modules
.env.local
.env.development.local
.env.test.local
.env.production.local
# OS
.DS_Store
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Project
coverage
@@ -26,11 +16,13 @@ apps/backend/vercel_token
apps/backend-copy
apps/frontend/.env
apps/frontend/src/backend
apps/frontend/build
# Astro's generated types and content store
apps/frontend/.astro
build-ts
tsconfig.tsbuildinfo
.turbo
build
*.tsbuildinfo
# IDE
.idea/
+1 -2
View File
@@ -1,4 +1,3 @@
pnpm lint-staged
pnpm run lint
# TODO enable
# npm test
pnpm test
-4
View File
@@ -1,5 +1 @@
pnpm-lock.yaml
# https://github.com/stats-organization/github-stats-extended/pull/26#discussion_r2709033732
# Avoid prettier format on md files to simplify merge on upstream repo
*.md
+1 -1
View File
@@ -1,7 +1,7 @@
{
"recommendations": [
"yzhang.markdown-all-in-one",
"prettier.prettier-vscode",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"github.vscode-github-actions"
]
+1 -2
View File
@@ -3,8 +3,7 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"cSpell.words": ["Wakatime"]
"cSpell.words": ["retryer", "Wakatime"]
}
+32 -23
View File
@@ -5,29 +5,40 @@
<a href="https://github-stats-extended.vercel.app/api?username=anuraghazra"><img src="https://github-stats-extended.vercel.app/api?username=anuraghazra"></a>
</div>
This project is an [extended version](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates [various stats cards](#card-types), e.g. about your GitHub contributions, your top languages, etc. You can [customize](#advanced-customization) the cards via multiple parameters.
GitHub-Stats-Extended is the [extended, actively maintained successor](docs/fork.md) of [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). It generates [various stats cards](#card-types) about your GitHub contributions, your top languages and more. You can [customize](#documentation) the cards via multiple parameters.
## Table of Contents
# Table of Contents
- [Quick Start](#quick-start)
- [Migration from github-readme-stats](#migration-from-github-readme-stats)
- [Card Types](#card-types)
- [Advanced Customization](#advanced-customization)
- [Self-Hosting](#self-hosting)
- [Documentation](#documentation)
- [Acknowledgements](#acknowledgements)
- [Contributing](#contributing)
# Quick Start
- Copy and paste this into your markdown:
```markdown
[![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)](https://github.com/stats-organization/github-stats-extended)
```
- Change the `?username=` value to your GitHub username.
- Done!
## Quick Start
-----------------------
Copy and paste this into your markdown, then change the `?username=` value to your GitHub username:
As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) to create your custom stats card. Copy the generated markdown code and paste it into your [GitHub profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme). Done!
```md
[![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)](https://github.com/stats-organization/github-stats-extended)
```
As a more comfortable alternative, use the [card wizard](https://github-stats-extended.vercel.app/frontend) to configure your card visually. Then copy the generated markdown into your [GitHub profile README](https://docs.github.com/en/account-and-profile/how-tos/profile-customization/managing-your-profile-readme#adding-a-profile-readme).
## Migration from github-readme-stats
To migrate from [github-readme-stats](https://github.com/anuraghazra/github-readme-stats) you only need to change the domain from `github-readme-stats.vercel.app` to `github-stats-extended.vercel.app`:
```diff
- https://github-readme-stats.vercel.app/api?username=octocat&theme=radical
+ https://github-stats-extended.vercel.app/api?username=octocat&theme=radical
```
GitHub-Stats-Extended aims to be fully compatible with github-readme-stats. For details see [Compatibility Notes](docs/fork.md#compatibility-notes).
## Card Types
# Card Types
- Show your GitHub statistics:
![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra)
@@ -52,16 +63,14 @@ As more comfortable alternative, use the [GitHub-Stats-Extended Wizard](https://
[![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api/?username=anuraghazra&show_icons=true&theme=calm&rank_icon=github&include_all_commits=true&custom_title=Anurag's+Stats&disable_animations=true&number_format=long&show=prs_merged_percentage,prs_reviewed)](https://github-stats-extended.vercel.app/api/?username=anuraghazra&show_icons=true&theme=calm&rank_icon=github&include_all_commits=true&custom_title=Anurag's+Stats&disable_animations=true&number_format=long&show=prs_merged_percentage,prs_reviewed)
# Advanced Customization
The [GitHub-Stats-Extended Wizard](https://github-stats-extended.vercel.app/frontend) offers some essential customization options. For more advanced customization check out the [advanced documentation](docs/advanced_documentation.md).
## Documentation
# Acknowledgements
This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). On top of their functionality I added several new features and improvements. See [Fork Information](docs/fork.md) for a list of changes. The frontend I added to the project is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️
The [card wizard](https://github-stats-extended.vercel.app/frontend) offers some essential customization options. For more advanced customization and other project info check out the [advanced documentation](docs/advanced_documentation.md).
# Self-Hosting
Since the GitHub API only allows a limited number of requests per hour, the public instance of GitHub-Stats-Extended at https://github-stats-extended.vercel.app/api could possibly hit the rate limiter. If you host your own instance you do not have to worry about anything. Also, if you don't want to give my GitHub-Stats-Extended instance access to your private contributions but still want to include these contributions in your stats, you can simply host your own instance.
## Acknowledgements
See [Deploy on your own](docs/deploy.md) for various deployment options.
This project is based on [github-readme-stats](https://github.com/anuraghazra/github-readme-stats). The card wizard is based on [GitHub Trends](https://github.com/avgupta456/github-trends). Big thanks to [@anuraghazra](https://github.com/anuraghazra), [@avgupta456](https://github.com/avgupta456), [@rickstaa](https://github.com/rickstaa), [@qwerty541](https://github.com/qwerty541) and everyone else who worked on these projects! ❤️
# Contributing
Contributions are welcome!
## Contributing
Contributions are welcome!
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "GitHub Stats Extended Dev",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/node:1": { "version": "22" }
"ghcr.io/devcontainers/features/node:1": { "version": "24" }
},
"forwardPorts": [3000],
"portsAttributes": {
-2
View File
@@ -5,7 +5,5 @@ coverage
scripts
tests
.env
**/*.md
**/*.svg
.eslintrc.json
codecov.yml
@@ -1,5 +1,5 @@
{
"runtime": "nodejs22.x",
"runtime": "nodejs24.x",
"handler": "router.js",
"launcherType": "Nodejs"
}
@@ -1,76 +0,0 @@
/* eslint-disable import-x/no-unresolved */
import { default as authenticate } from "./api-renamed/authenticate.js";
import { default as deleteUser } from "./api-renamed/delete-user.js";
import { default as downgrade } from "./api-renamed/downgrade.js";
import { default as gist } from "./api-renamed/gist.js";
import { default as api } from "./api-renamed/index.js";
import { default as pin } from "./api-renamed/pin.js";
import { default as repeatRecent } from "./api-renamed/repeat-recent.js";
import { default as patInfo } from "./api-renamed/status/pat-info.js";
import { default as statusUp } from "./api-renamed/status/up.js";
import { default as topLangs } from "./api-renamed/top-langs.js";
import { default as userAccess } from "./api-renamed/user-access.js";
import { default as wakatimeProxy } from "./api-renamed/wakatime-proxy.js";
import { default as wakatime } from "./api-renamed/wakatime.js";
export default async (req, res) => {
// remaining code expects express.js-like request and response objects
res.send = function (data) {
if (typeof data === "object") {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(data));
} else if (typeof data === "string") {
res.end(data);
} else {
res.end(String(data));
}
};
const url = new URL(req.url, "https://localhost");
req.query = Object.fromEntries(url.searchParams.entries());
switch (url.pathname) {
case "/api":
await api(req, res);
break;
case "/api/gist":
await gist(req, res);
break;
case "/api/pin":
await pin(req, res);
break;
case "/api/top-langs":
await topLangs(req, res);
break;
case "/api/wakatime":
await wakatime(req, res);
break;
case "/api/wakatime-proxy":
await wakatimeProxy(req, res);
break;
case "/api/repeat-recent":
await repeatRecent(req, res);
break;
case "/api/status/pat-info":
await patInfo(req, res);
break;
case "/api/status/up":
await statusUp(req, res);
break;
case "/api/authenticate":
await authenticate(req, res);
break;
case "/api/delete-user":
await deleteUser(req, res);
break;
case "/api/user-access":
await userAccess(req, res);
break;
case "/api/downgrade":
await downgrade(req, res);
break;
default:
res.statusCode = 404;
res.end("Not Found");
break;
}
};
@@ -1,5 +1,5 @@
{
"expiration": 39600,
"expiration": 255600,
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
"passQuery": true
}
@@ -1,5 +1,5 @@
{
"expiration": 39600,
"expiration": 255600,
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
"passQuery": true
}
@@ -1,5 +1,5 @@
{
"expiration": 39600,
"expiration": 255600,
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
"passQuery": true
}
@@ -1,5 +1,5 @@
{
"expiration": 39600,
"expiration": 255600,
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
"passQuery": true
}
@@ -1,5 +1,5 @@
{
"expiration": 39600,
"expiration": 255600,
"bypassToken": "r3fr3shT0k3n-r3fr3shT0k3n-r3fr3shT0k3n",
"passQuery": true
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { logger } from "../src/common/log.js";
import { logger } from "@stats-organization/github-readme-stats-core";
import { authenticate } from "../src/users.js";
/**
+2 -1
View File
@@ -1,5 +1,6 @@
import { logger } from "@stats-organization/github-readme-stats-core";
import { deleteUser } from "../src/common/database.js";
import { logger } from "../src/common/log.js";
/**
* @param {any} req The request.
+1 -1
View File
@@ -1,7 +1,7 @@
import { logger } from "@stats-organization/github-readme-stats-core";
import axios from "axios";
import { deleteUser, getUserAccessByKey } from "../src/common/database.js";
import { logger } from "../src/common/log.js";
export default async (req, res) => {
// We could optimize this method by doing both database operations in one statement, using "DELETE ... RETURNING ..."
-129
View File
@@ -1,129 +0,0 @@
// @ts-check
import { renderGistCard } from "../src/cards/gist.js";
import { guardAccess } from "../src/common/access.js";
import {
CACHE_TTL,
resolveCacheSeconds,
setCacheHeaders,
setErrorCacheHeaders,
} from "../src/common/cache.js";
import { storeRequest } from "../src/common/database.js";
import {
MissingParamError,
retrieveSecondaryMessage,
} from "../src/common/error.js";
import { parseBoolean } from "../src/common/ops.js";
import { renderError } from "../src/common/render.js";
import { fetchGist } from "../src/fetchers/gist.js";
import { isLocaleAvailable } from "../src/translations.js";
// @ts-ignore
export default async (req, res) => {
const {
id,
title_color,
icon_color,
text_color,
bg_color,
theme,
cache_seconds,
locale,
border_radius,
border_color,
show_owner,
hide_border,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
const access = guardAccess({
res,
id,
type: "gist",
colors: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
});
if (!access.isPassed) {
return access.result;
}
if (locale && !isLocaleAvailable(locale)) {
return res.send(
renderError({
message: "Something went wrong",
secondaryMessage: "Language not found",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
try {
await storeRequest(req);
const gistData = await fetchGist(id);
const cacheSeconds = resolveCacheSeconds({
requested: parseInt(cache_seconds, 10),
def: CACHE_TTL.GIST_CARD.DEFAULT,
min: CACHE_TTL.GIST_CARD.MIN,
max: CACHE_TTL.GIST_CARD.MAX,
});
setCacheHeaders(res, cacheSeconds);
return res.send(
renderGistCard(gistData, {
title_color,
icon_color,
text_color,
bg_color,
theme,
border_radius,
border_color,
locale: locale ? locale.toLowerCase() : null,
show_owner: parseBoolean(show_owner),
hide_border: parseBoolean(hide_border),
}),
);
} catch (err) {
setErrorCacheHeaders(res);
if (err instanceof Error) {
return res.send(
renderError({
message: err.message,
secondaryMessage: retrieveSecondaryMessage(err),
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
show_repo_link: !(err instanceof MissingParamError),
},
}),
);
}
return res.send(
renderError({
message: "An unknown error occurred",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
};
-175
View File
@@ -1,175 +0,0 @@
// @ts-check
import { renderRepoCard } from "../src/cards/repo.js";
import { guardAccess } from "../src/common/access.js";
import {
CACHE_TTL,
resolveCacheSeconds,
setCacheHeaders,
setErrorCacheHeaders,
} from "../src/common/cache.js";
import { storeRequest } from "../src/common/database.js";
import {
MissingParamError,
retrieveSecondaryMessage,
} from "../src/common/error.js";
import { parseArray, parseBoolean } from "../src/common/ops.js";
import { renderError } from "../src/common/render.js";
import { fetchRepo } from "../src/fetchers/repo.js";
import { isLocaleAvailable } from "../src/translations.js";
// @ts-ignore
export default async (req, res) => {
const {
username,
repo,
hide_border,
title_color,
icon_color,
text_color,
bg_color,
card_width,
theme,
show_owner,
show,
show_icons,
number_format,
text_bold,
line_height,
cache_seconds,
locale,
border_radius,
border_color,
description_lines_count,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
const access = guardAccess({
res,
id: username,
type: "username",
colors: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
});
if (!access.isPassed) {
return access.result;
}
if (locale && !isLocaleAvailable(locale)) {
return res.send(
renderError({
message: "Something went wrong",
secondaryMessage: "Language not found",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
const safePattern = /^[-\w/.,]+$/;
if (
(username && !safePattern.test(username)) ||
(repo && !safePattern.test(repo))
) {
return res.send(
renderError({
message: "Something went wrong",
secondaryMessage: "Username or repository contains unsafe characters",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
try {
await storeRequest(req);
const showStats = parseArray(show);
const repoData = await fetchRepo(
username,
repo,
showStats.includes("prs_authored"),
showStats.includes("prs_commented"),
showStats.includes("prs_reviewed"),
showStats.includes("issues_authored"),
showStats.includes("issues_commented"),
);
const cacheSeconds = resolveCacheSeconds({
requested: parseInt(cache_seconds, 10),
def: CACHE_TTL.PIN_CARD.DEFAULT,
min: CACHE_TTL.PIN_CARD.MIN,
max: CACHE_TTL.PIN_CARD.MAX,
});
setCacheHeaders(res, cacheSeconds);
return res.send(
renderRepoCard(repoData, {
hide_border: parseBoolean(hide_border),
title_color,
icon_color,
text_color,
bg_color,
theme,
border_radius,
border_color,
card_width_input: parseInt(card_width, 10),
show_owner: parseBoolean(show_owner),
show: showStats,
show_icons: parseBoolean(show_icons),
number_format,
text_bold: parseBoolean(text_bold),
line_height,
username,
locale: locale ? locale.toLowerCase() : null,
description_lines_count,
}),
);
} catch (err) {
setErrorCacheHeaders(res);
if (err instanceof Error) {
return res.send(
renderError({
message: err.message,
secondaryMessage: retrieveSecondaryMessage(err),
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
show_repo_link: !(err instanceof MissingParamError),
},
}),
);
}
return res.send(
renderError({
message: "An unknown error occurred",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
};
+13 -11
View File
@@ -6,10 +6,12 @@
*
* @description This function is currently rate limited to 1 request per 3 minutes.
*/
import { request } from "../../src/common/http.js";
import { logger } from "../../src/common/log.js";
import { dateDiff } from "../../src/common/ops.js";
import {
dateDiff,
getConfig,
logger,
request,
} from "@stats-organization/github-readme-stats-core";
export const RATE_LIMIT_SECONDS = 60 * 3; // 1 request per 3 minutes
@@ -39,7 +41,7 @@ const uptimeFetcher = (variables, token) => {
};
const getAllPATs = () => {
return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key));
return getConfig().pats;
};
/**
@@ -61,7 +63,7 @@ const getPATInfo = async (fetcher, variables) => {
for (const pat of PATs) {
try {
const response = await fetcher(variables, process.env[pat]);
const response = await fetcher(variables, pat.value);
const errors = response.data.errors;
const hasErrors = Boolean(errors);
const errorType = errors?.[0]?.type;
@@ -71,7 +73,7 @@ const getPATInfo = async (fetcher, variables) => {
// Store PATs with errors.
if (hasErrors && errorType !== "RATE_LIMITED") {
details[pat] = {
details[pat.name] = {
status: "error",
error: {
type: errors[0].type,
@@ -82,13 +84,13 @@ const getPATInfo = async (fetcher, variables) => {
} else if (isRateLimited) {
const date1 = new Date();
const date2 = new Date(response.data?.data?.rateLimit?.resetAt);
details[pat] = {
details[pat.name] = {
status: "exhausted",
remaining: 0,
resetIn: dateDiff(date2, date1) + " minutes",
};
} else {
details[pat] = {
details[pat.name] = {
status: "valid",
remaining: response.data.data.rateLimit.remaining,
};
@@ -97,11 +99,11 @@ const getPATInfo = async (fetcher, variables) => {
// Store the PAT if it is expired.
const errorMessage = err.response?.data?.message?.toLowerCase();
if (errorMessage === "bad credentials") {
details[pat] = {
details[pat.name] = {
status: "expired",
};
} else if (errorMessage === "sorry. your account was suspended.") {
details[pat] = {
details[pat.name] = {
status: "suspended",
};
} else {
+6 -4
View File
@@ -7,9 +7,11 @@
* @description This function is currently rate limited to 1 request per 3 minutes.
*/
import { request } from "../../src/common/http.js";
import { logger } from "../../src/common/log.js";
import { default as retryer } from "../../src/common/retryer.js";
import {
logger,
request,
retryer,
} from "@stats-organization/github-readme-stats-core";
export const RATE_LIMIT_SECONDS = 60 * 3; // 1 request per 3 minutes
@@ -87,7 +89,7 @@ export default async (req, res) => {
try {
let PATsValid = true;
try {
await retryer(uptimeFetcher, null, {});
await retryer(uptimeFetcher, {});
} catch (err) {
// Resolve eslint no-unused-vars
err;
-194
View File
@@ -1,194 +0,0 @@
// @ts-check
import { renderTopLanguages } from "../src/cards/top-languages.js";
import { guardAccess } from "../src/common/access.js";
import {
CACHE_TTL,
resolveCacheSeconds,
setCacheHeaders,
setErrorCacheHeaders,
} from "../src/common/cache.js";
import { storeRequest } from "../src/common/database.js";
import {
MissingParamError,
retrieveSecondaryMessage,
} from "../src/common/error.js";
import { parseArray, parseBoolean } from "../src/common/ops.js";
import { renderError } from "../src/common/render.js";
import { fetchTopLanguages } from "../src/fetchers/top-languages.js";
import { isLocaleAvailable } from "../src/translations.js";
// @ts-ignore
export default async (req, res) => {
const {
username,
hide,
hide_title,
hide_border,
card_width,
title_color,
text_color,
bg_color,
prog_bar_bg_color,
theme,
cache_seconds,
layout,
langs_count,
exclude_repo,
size_weight,
count_weight,
custom_title,
locale,
border_radius,
border_color,
role,
disable_animations,
hide_progress,
stats_format,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
const access = guardAccess({
res,
id: username,
type: "username",
colors: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
});
if (!access.isPassed) {
return access.result;
}
if (locale && !isLocaleAvailable(locale)) {
return res.send(
renderError({
message: "Something went wrong",
secondaryMessage: "Locale not found",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
if (
layout !== undefined &&
(typeof layout !== "string" ||
!["compact", "normal", "donut", "donut-vertical", "pie"].includes(layout))
) {
return res.send(
renderError({
message: "Something went wrong",
secondaryMessage: "Incorrect layout input",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
if (
stats_format !== undefined &&
(typeof stats_format !== "string" ||
!["bytes", "percentages"].includes(stats_format))
) {
return res.send(
renderError({
message: "Something went wrong",
secondaryMessage: "Incorrect stats_format input",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
try {
await storeRequest(req);
const topLangs = await fetchTopLanguages(
username,
parseArray(exclude_repo),
size_weight,
count_weight,
parseArray(role),
);
const cacheSeconds = resolveCacheSeconds({
requested: parseInt(cache_seconds, 10),
def: CACHE_TTL.TOP_LANGS_CARD.DEFAULT,
min: CACHE_TTL.TOP_LANGS_CARD.MIN,
max: CACHE_TTL.TOP_LANGS_CARD.MAX,
});
setCacheHeaders(res, cacheSeconds);
return res.send(
renderTopLanguages(topLangs, {
custom_title,
hide_title: parseBoolean(hide_title),
hide_border: parseBoolean(hide_border),
card_width: parseInt(card_width, 10),
hide: parseArray(hide),
title_color,
text_color,
bg_color,
prog_bar_bg_color,
theme,
layout,
langs_count,
border_radius,
border_color,
locale: locale ? locale.toLowerCase() : null,
disable_animations: parseBoolean(disable_animations),
hide_progress: parseBoolean(hide_progress),
stats_format,
}),
);
} catch (err) {
setErrorCacheHeaders(res);
if (err instanceof Error) {
return res.send(
renderError({
message: err.message,
secondaryMessage: retrieveSecondaryMessage(err),
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
show_repo_link: !(err instanceof MissingParamError),
},
}),
);
}
return res.send(
renderError({
message: "An unknown error occurred",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
};
+2 -1
View File
@@ -1,5 +1,6 @@
import { logger } from "@stats-organization/github-readme-stats-core";
import { getUserAccessByKey } from "../src/common/database.js";
import { logger } from "../src/common/log.js";
/**
* @param {any} req The request.
+4 -2
View File
@@ -1,5 +1,7 @@
import { logger } from "../src/common/log.js";
import { fetchWakatimeStats } from "../src/fetchers/wakatime.js";
import {
fetchWakatimeStats,
logger,
} from "@stats-organization/github-readme-stats-core";
/**
* @param {any} req The request.
-148
View File
@@ -1,148 +0,0 @@
// @ts-check
import { renderWakatimeCard } from "../src/cards/wakatime.js";
import { guardAccess } from "../src/common/access.js";
import {
CACHE_TTL,
resolveCacheSeconds,
setCacheHeaders,
setErrorCacheHeaders,
} from "../src/common/cache.js";
import { storeRequest } from "../src/common/database.js";
import {
MissingParamError,
retrieveSecondaryMessage,
} from "../src/common/error.js";
import { parseArray, parseBoolean } from "../src/common/ops.js";
import { renderError } from "../src/common/render.js";
import { fetchWakatimeStats } from "../src/fetchers/wakatime.js";
import { isLocaleAvailable } from "../src/translations.js";
// @ts-ignore
export default async (req, res) => {
const {
username,
title_color,
icon_color,
hide_border,
card_width,
line_height,
text_color,
bg_color,
theme,
cache_seconds,
hide_title,
hide_progress,
custom_title,
locale,
layout,
langs_count,
hide,
api_domain,
border_radius,
border_color,
display_format,
disable_animations,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
const access = guardAccess({
res,
id: username,
type: "wakatime",
colors: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
});
if (!access.isPassed) {
return access.result;
}
if (locale && !isLocaleAvailable(locale)) {
return res.send(
renderError({
message: "Something went wrong",
secondaryMessage: "Language not found",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
try {
await storeRequest(req);
const stats = await fetchWakatimeStats({ username, api_domain });
const cacheSeconds = resolveCacheSeconds({
requested: parseInt(cache_seconds, 10),
def: CACHE_TTL.WAKATIME_CARD.DEFAULT,
min: CACHE_TTL.WAKATIME_CARD.MIN,
max: CACHE_TTL.WAKATIME_CARD.MAX,
});
setCacheHeaders(res, cacheSeconds);
return res.send(
renderWakatimeCard(stats, {
custom_title,
hide_title: parseBoolean(hide_title),
hide_border: parseBoolean(hide_border),
card_width: parseInt(card_width, 10),
hide: parseArray(hide),
line_height,
title_color,
icon_color,
text_color,
bg_color,
theme,
hide_progress,
border_radius,
border_color,
locale: locale ? locale.toLowerCase() : null,
layout,
langs_count,
display_format,
disable_animations: parseBoolean(disable_animations),
}),
);
} catch (err) {
setErrorCacheHeaders(res);
if (err instanceof Error) {
return res.send(
renderError({
message: err.message,
secondaryMessage: retrieveSecondaryMessage(err),
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
show_repo_link: !(err instanceof MissingParamError),
},
}),
);
}
return res.send(
renderError({
message: "An unknown error occurred",
renderOptions: {
title_color,
text_color,
bg_color,
border_color,
theme,
},
}),
);
}
};
+12 -13
View File
@@ -1,21 +1,20 @@
import express from "express";
import gistCard from "./api-renamed/gist.js";
import statsCard from "./api-renamed/index.js";
import repoCard from "./api-renamed/pin.js";
import langCard from "./api-renamed/top-langs.js";
import wakatimeCard from "./api-renamed/wakatime.js";
import router from "./router.js";
const app = express();
const router = express.Router();
router.get("/", statsCard);
router.get("/pin", repoCard);
router.get("/top-langs", langCard);
router.get("/wakatime", wakatimeCard);
router.get("/gist", gistCard);
app.use("/api", router);
app.use((req, res) => {
/*
* Vercel resolves `/api/pin/` to the same function as `/api/pin`, but the router matches the path exactly.
* Drop the trailing slash so local requests behave like deployed ones.
*/
const [pathname, query] = req.url.split("?", 2);
if (pathname !== "/" && pathname?.endsWith("/")) {
req.url = pathname.slice(0, -1) + (query === undefined ? "" : `?${query}`);
}
return router(req, res);
});
const port = process.env.PORT || process.env.port || 9000;
app.listen(port, "0.0.0.0", () => {
+1
View File
@@ -0,0 +1 @@
export { default as router } from "./router.js";
+16 -38
View File
@@ -1,52 +1,30 @@
{
"name": "github-readme-stats",
"version": "1.0.0",
"description": "Dynamically generate stats for your GitHub readme",
"keywords": [
"github-readme-stats",
"readme-stats",
"cards",
"card-generator"
],
"main": "src/index.js",
"name": "@stats-organization/github-readme-stats-backend",
"type": "module",
"homepage": "https://github-stats-extended.vercel.app/frontend",
"bugs": {
"url": "https://github.com/stats-organization/github-stats-extended/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/stats-organization/github-stats-extended.git"
"license": "MIT",
"engines": {
"node": "24.x"
},
"private": true,
"main": "index.js",
"scripts": {
"dev": "node --env-file-if-exists=.env express.js",
"test": "vitest",
"test:update:snapshot": "vitest -u",
"test:e2e": "vitest --config vitest.config.e2e.ts",
"theme-readme-gen": "node scripts/generate-theme-doc",
"generate-langs-json": "node scripts/generate-langs-json",
"bench": "vitest bench --run --config vitest.config.bench.ts"
"bench": "vitest bench --run --config vitest.config.bench.ts",
"lint": "eslint",
"typecheck": "tsc -p tsconfig.typecheck.json"
},
"author": "Anurag Hazra",
"license": "MIT",
"devDependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@uppercod/css-to-object": "^1.1.1",
"@vitest/coverage-v8": "catalog:default",
"axios-mock-adapter": "^2.1.0",
"express": "^5.2.1",
"js-yaml": "^4.1.1",
"jsdom": "28.1.0",
"axios-mock-adapter": "2.1.0",
"express": "5.2.1",
"jsdom": "catalog:default",
"vitest": "catalog:default"
},
"dependencies": {
"axios": "^1.13.5",
"emoji-name-map": "^2.0.3",
"github-username-regex": "^1.0.0",
"pg": "^8.18.0",
"word-wrap": "^1.2.5"
},
"engines": {
"node": "24.x"
"@stats-organization/github-readme-stats-core": "workspace:^",
"axios": "catalog:default",
"pg": "^8.22.0"
}
}
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 KiB

+225
View File
@@ -0,0 +1,225 @@
import {
api,
gist,
pin,
topLangs,
wakatime,
} from "@stats-organization/github-readme-stats-core";
import { default as authenticate } from "./api-renamed/authenticate.js";
import { default as deleteUser } from "./api-renamed/delete-user.js";
import { default as downgrade } from "./api-renamed/downgrade.js";
import { default as repeatRecent } from "./api-renamed/repeat-recent.js";
import { default as patInfo } from "./api-renamed/status/pat-info.js";
import { default as statusUp } from "./api-renamed/status/up.js";
import { default as userAccess } from "./api-renamed/user-access.js";
import { default as wakatimeProxy } from "./api-renamed/wakatime-proxy.js";
import { guardAccess } from "./src/common/access.js";
import {
CACHE_TTL,
resolveCacheSeconds,
setCacheHeaders,
setErrorCacheHeaders,
} from "./src/common/cache.js";
import { getUserAccessByName, storeRequest } from "./src/common/database.js";
const getGuardResult = (query, type, id) => {
const access = guardAccess({
id,
type,
colors: {
title_color: query.title_color,
text_color: query.text_color,
bg_color: query.bg_color,
border_color: query.border_color,
theme: query.theme,
},
});
if (access.isPassed) {
return null;
}
return {
status: "error - permanent",
content: access.result,
};
};
const getUserPat = async (username) => {
if (!username) {
return null;
}
const userAccess = await getUserAccessByName(username);
if (!userAccess?.token) {
return null;
}
return userAccess.token;
};
export default async (req, res) => {
const url = new URL(req.url, "https://localhost");
if (res.send === undefined) {
// remaining code expects express.js-like request and response objects
res.send = function (data) {
if (typeof data === "object") {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(data));
} else if (typeof data === "string") {
res.end(data);
} else {
res.end(String(data));
}
};
req.query = Object.fromEntries(url.searchParams.entries());
}
let result;
switch (url.pathname) {
case "/api": {
result = getGuardResult(req.query, "username", req.query.username);
if (!result) {
const userPat = await getUserPat(req.query.username);
result = await api(req.query, userPat);
}
if (result.status === "error - temporary") {
setErrorCacheHeaders(res);
} else {
const cacheSeconds = resolveCacheSeconds({
requested: parseInt(req.query.cache_seconds, 10),
def: CACHE_TTL.STATS_CARD.DEFAULT,
min: CACHE_TTL.STATS_CARD.MIN,
max: CACHE_TTL.STATS_CARD.MAX,
});
setCacheHeaders(res, cacheSeconds);
}
res.setHeader("Content-Type", "image/svg+xml");
res.end(result.content);
if (result.status !== "error - permanent") {
await storeRequest(req);
}
break;
}
case "/api/gist":
result =
getGuardResult(req.query, "gist", req.query.id) ??
(await gist(req.query));
if (result.status === "error - temporary") {
setErrorCacheHeaders(res);
} else {
const cacheSeconds = resolveCacheSeconds({
requested: parseInt(req.query.cache_seconds, 10),
def: CACHE_TTL.GIST_CARD.DEFAULT,
min: CACHE_TTL.GIST_CARD.MIN,
max: CACHE_TTL.GIST_CARD.MAX,
});
setCacheHeaders(res, cacheSeconds);
}
res.setHeader("Content-Type", "image/svg+xml");
res.end(result.content);
if (result.status !== "error - permanent") {
await storeRequest(req);
}
break;
case "/api/pin": {
result = getGuardResult(req.query, "username", req.query.username);
if (!result) {
const userPat = await getUserPat(req.query.username);
result = await pin(req.query, userPat);
}
if (result.status === "error - temporary") {
setErrorCacheHeaders(res);
} else {
const cacheSeconds = resolveCacheSeconds({
requested: parseInt(req.query.cache_seconds, 10),
def: CACHE_TTL.PIN_CARD.DEFAULT,
min: CACHE_TTL.PIN_CARD.MIN,
max: CACHE_TTL.PIN_CARD.MAX,
});
setCacheHeaders(res, cacheSeconds);
}
res.setHeader("Content-Type", "image/svg+xml");
res.end(result.content);
if (result.status !== "error - permanent") {
await storeRequest(req);
}
break;
}
case "/api/top-langs": {
result = getGuardResult(req.query, "username", req.query.username);
if (!result) {
const userPat = await getUserPat(req.query.username);
result = await topLangs(req.query, userPat);
}
if (result.status === "error - temporary") {
setErrorCacheHeaders(res);
} else {
const cacheSeconds = resolveCacheSeconds({
requested: parseInt(req.query.cache_seconds, 10),
def: CACHE_TTL.TOP_LANGS_CARD.DEFAULT,
min: CACHE_TTL.TOP_LANGS_CARD.MIN,
max: CACHE_TTL.TOP_LANGS_CARD.MAX,
});
setCacheHeaders(res, cacheSeconds);
}
res.setHeader("Content-Type", "image/svg+xml");
res.end(result.content);
if (result.status !== "error - permanent") {
await storeRequest(req);
}
break;
}
case "/api/wakatime":
result =
getGuardResult(req.query, "wakatime", req.query.username) ??
(await wakatime(req.query));
if (result.status === "error - temporary") {
setErrorCacheHeaders(res);
} else {
const cacheSeconds = resolveCacheSeconds({
requested: parseInt(req.query.cache_seconds, 10),
def: CACHE_TTL.WAKATIME_CARD.DEFAULT,
min: CACHE_TTL.WAKATIME_CARD.MIN,
max: CACHE_TTL.WAKATIME_CARD.MAX,
});
setCacheHeaders(res, cacheSeconds);
}
res.setHeader("Content-Type", "image/svg+xml");
res.end(result.content);
if (result.status !== "error - permanent") {
await storeRequest(req);
}
break;
case "/api/wakatime-proxy":
await wakatimeProxy(req, res);
break;
case "/api/repeat-recent":
await repeatRecent(req, res);
break;
case "/api/status/pat-info":
await patInfo(req, res);
break;
case "/api/status/up":
await statusUp(req, res);
break;
case "/api/authenticate":
await authenticate(req, res);
break;
case "/api/delete-user":
await deleteUser(req, res);
break;
case "/api/user-access":
await userAccess(req, res);
break;
case "/api/downgrade":
await downgrade(req, res);
break;
default:
res.statusCode = 404;
res.end("Not Found");
break;
}
};
-141
View File
@@ -1,141 +0,0 @@
// @ts-check
import { default as Card } from "../common/Card.js";
import { getCardColors } from "../common/color.js";
import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
import { encodeHTML } from "../common/html.js";
import { icons } from "../common/icons.js";
import languageColors from "../common/languageColors.json" with { type: "json" };
import { parseEmojis } from "../common/ops.js";
import {
createLanguageNode,
flexLayout,
iconWithLabel,
measureText,
} from "../common/render.js";
const ICON_SIZE = 16;
const CARD_DEFAULT_WIDTH = 400;
const HEADER_MAX_LENGTH = 35;
/**
* @typedef {import('./types').GistCardOptions} GistCardOptions Gist card options.
* @typedef {import('../fetchers/types').GistData} GistData Gist data.
*/
/**
* Render gist card.
*
* @param {GistData} gistData Gist data.
* @param {Partial<GistCardOptions>} options Gist card options.
* @returns {string} Gist card.
*/
const renderGistCard = (gistData, options = {}) => {
const { name, nameWithOwner, description, language, starsCount, forksCount } =
gistData;
const {
title_color,
icon_color,
text_color,
bg_color,
theme = "default_repocard",
border_radius,
border_color,
show_owner = false,
hide_border = false,
} = options;
// returns theme based colors with proper overrides and defaults
const { titleColor, textColor, iconColor, bgColor, borderColor } =
getCardColors({
title_color,
icon_color,
text_color,
bg_color,
border_color,
theme,
});
const lineWidth = 59;
const linesLimit = 10;
const desc = parseEmojis(description || "No description provided");
const multiLineDescription = wrapTextMultiline(desc, lineWidth, linesLimit);
const descriptionLines = multiLineDescription.length;
const descriptionSvg = multiLineDescription
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
.join("");
const lineHeight = descriptionLines > 3 ? 12 : 10;
const height =
(descriptionLines > 1 ? 120 : 110) + descriptionLines * lineHeight;
const totalStars = kFormatter(starsCount);
const totalForks = kFormatter(forksCount);
const svgStars = iconWithLabel(
icons.star,
totalStars,
"starsCount",
ICON_SIZE,
);
const svgForks = iconWithLabel(
icons.fork,
totalForks,
"forksCount",
ICON_SIZE,
);
const languageName = language || "Unspecified";
// @ts-ignore
const languageColor = languageColors[languageName] || "#858585";
const svgLanguage = createLanguageNode(languageName, languageColor);
const starAndForkCount = flexLayout({
items: [svgLanguage, svgStars, svgForks],
sizes: [
measureText(languageName, 12),
ICON_SIZE + measureText(`${totalStars}`, 12),
ICON_SIZE + measureText(`${totalForks}`, 12),
],
gap: 25,
}).join("");
const header = show_owner ? nameWithOwner : name;
const card = new Card({
defaultTitle:
header.length > HEADER_MAX_LENGTH
? `${header.slice(0, HEADER_MAX_LENGTH)}...`
: header,
titlePrefixIcon: icons.gist,
width: CARD_DEFAULT_WIDTH,
height,
border_radius,
colors: {
titleColor,
textColor,
iconColor,
bgColor,
borderColor,
},
});
card.setCSS(`
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
.icon { fill: ${iconColor} }
`);
card.setHideBorder(hide_border);
return card.render(`
<text class="description" x="25" y="-5">
${descriptionSvg}
</text>
<g transform="translate(30, ${height - 75})">
${starAndForkCount}
</g>
`);
};
export { renderGistCard };
-4
View File
@@ -1,4 +0,0 @@
export { renderRepoCard } from "./repo.js";
export { renderStatsCard } from "./stats.js";
export { renderTopLanguages } from "./top-languages.js";
export { renderWakatimeCard } from "./wakatime.js";
-324
View File
@@ -1,324 +0,0 @@
// @ts-check
import { Card } from "../common/Card.js";
import { I18n } from "../common/I18n.js";
import { getCardColors } from "../common/color.js";
import { kFormatter, wrapTextMultiline } from "../common/fmt.js";
import { encodeHTML } from "../common/html.js";
import { icons } from "../common/icons.js";
import { buildSearchFilter, clampValue, parseEmojis } from "../common/ops.js";
import {
createLanguageNode,
flexLayout,
iconWithLabel,
measureText,
} from "../common/render.js";
import { repoCardLocales } from "../translations.js";
import { createTextNode } from "./stats.js";
const ICON_SIZE = 16;
const DESCRIPTION_LINE_WIDTH = 59;
const DESCRIPTION_MAX_LINES = 3;
/**
* Retrieves the repository description and wraps it to fit the card width.
*
* @param {string} label The repository description.
* @param {string} textColor The color of the text.
* @returns {string} Wrapped repo description SVG object.
*/
const getBadgeSVG = (label, textColor, xOffset = 0) => `
<g data-testid="badge" class="badge" transform="translate(${320 + xOffset}, -18)">
<rect stroke="${textColor}" stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"></rect>
<text
x="23" y="-5"
alignment-baseline="central"
dominant-baseline="central"
text-anchor="middle"
fill="${textColor}"
>
${label}
</text>
</g>
`;
/**
* @typedef {import("../fetchers/types").RepositoryData} RepositoryData Repository data.
* @typedef {import("./types").RepoCardOptions} RepoCardOptions Repo card options.
*/
/**
* Renders repository card details.
*
* @param {RepositoryData} repo Repository data.
* @param {Partial<RepoCardOptions>} options Card options.
* @returns {string} Repository card SVG object.
*/
const renderRepoCard = (repo, options = {}) => {
const {
name,
nameWithOwner,
description,
primaryLanguage,
isArchived,
isTemplate,
starCount,
forkCount,
totalPRsAuthored,
totalPRsCommented,
totalPRsReviewed,
totalIssuesAuthored,
totalIssuesCommented,
} = repo;
const {
hide_border = false,
title_color,
icon_color,
text_color,
bg_color,
card_width_input,
show_owner = false,
show = [],
show_icons = true,
number_format = "short",
text_bold = false,
line_height = 22,
username,
theme = "default_repocard",
border_radius,
border_color,
locale,
description_lines_count,
} = options;
const card_width =
card_width_input && !isNaN(card_width_input)
? card_width_input
: show.length >= 2
? 430
: 400;
const i18n = new I18n({
locale,
translations: repoCardLocales,
});
let repoFilter = encodeURIComponent(buildSearchFilter([nameWithOwner], []));
const STATS = {};
if (show.includes("prs_authored")) {
STATS.prs_authored = {
icon: icons.prs,
label: i18n.t("repocard.prs-authored"),
value: totalPRsAuthored,
id: "prs_authored",
link: `https://github.com/search?q=${repoFilter}author%3A${username}&amp;type=pullrequests`,
};
}
if (show.includes("prs_commented")) {
STATS.prs_commented = {
icon: icons.comments,
label: i18n.t("repocard.prs-commented"),
value: totalPRsCommented,
id: "prs_commented",
link: `https://github.com/search?q=${repoFilter}commenter%3A${username}+-author%3A${username}&amp;type=pullrequests`,
};
}
if (show.includes("prs_reviewed")) {
STATS.prs_reviewed = {
icon: icons.reviews,
label: i18n.t("repocard.prs-reviewed"),
value: totalPRsReviewed,
id: "prs_reviewed",
link: `https://github.com/search?q=${repoFilter}reviewed-by%3A${username}+-author%3A${username}&amp;type=pullrequests`,
};
}
if (show.includes("issues_authored")) {
STATS.issues_authored = {
icon: icons.issues,
label: i18n.t("repocard.issues-authored"),
value: totalIssuesAuthored,
id: "issues_authored",
link: `https://github.com/search?q=${repoFilter}author%3A${username}&amp;type=issues`,
};
}
if (show.includes("issues_commented")) {
STATS.issues_commented = {
icon: icons.discussions_started,
label: i18n.t("repocard.issues-commented"),
value: totalIssuesCommented,
id: "issues_commented",
link: `https://github.com/search?q=${repoFilter}commenter%3A${username}+-author%3A${username}&amp;type=issues`,
};
}
const statItems = Object.keys(STATS).map((key, index) =>
// create the text nodes, and pass index so that we can calculate the line spacing
createTextNode({
icon: STATS[key].icon,
label: STATS[key].label,
value: STATS[key].value,
id: STATS[key].id,
unitSymbol: STATS[key].unitSymbol,
index,
showIcons: show_icons,
shiftValuePos: 14.01,
bold: text_bold,
numberFormat: number_format,
link: STATS[key].link,
labelXOffset: 23,
}),
);
const extraLHeight = parseInt(String(line_height), 10);
const lineHeight = 10;
const header = show_owner ? nameWithOwner : name;
const langName = (primaryLanguage && primaryLanguage.name) || "Unspecified";
const langColor = (primaryLanguage && primaryLanguage.color) || "#333";
const descriptionMaxLines = description_lines_count
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
: DESCRIPTION_MAX_LINES;
const desc = parseEmojis(description || "No description provided");
const multiLineDescription = wrapTextMultiline(
desc,
Math.round((card_width - 400) / 5.93 + DESCRIPTION_LINE_WIDTH),
descriptionMaxLines,
);
const descriptionLinesCount = description_lines_count
? clampValue(description_lines_count, 1, DESCRIPTION_MAX_LINES)
: multiLineDescription.length;
const descriptionSvg = multiLineDescription
.map((line) => `<tspan dy="1.2em" x="25">${encodeHTML(line)}</tspan>`)
.join("");
const extraHeight = Object.keys(STATS).length
? -7 + (Math.ceil(statItems.length / 2) + 1) * extraLHeight
: 0;
const height =
(descriptionLinesCount > 1 ? 120 : 110) +
descriptionLinesCount * lineHeight +
extraHeight;
// returns theme based colors with proper overrides and defaults
const colors = getCardColors({
title_color,
icon_color,
text_color,
bg_color,
border_color,
theme,
});
const svgLanguage = primaryLanguage
? createLanguageNode(langName, langColor)
: "";
const totalStars = kFormatter(starCount);
const totalForks = kFormatter(forkCount);
const svgStars = iconWithLabel(
icons.star,
totalStars,
"stargazers",
ICON_SIZE,
);
const svgForks = iconWithLabel(
icons.fork,
totalForks,
"forkcount",
ICON_SIZE,
);
const starAndForkCount = flexLayout({
items: [svgLanguage, svgStars, svgForks],
sizes: [
measureText(langName, 12),
ICON_SIZE + measureText(`${totalStars}`, 12),
ICON_SIZE + measureText(`${totalForks}`, 12),
],
gap: 25,
}).join("");
let extraRows = [];
for (let i = 0; i < statItems.length; i += 2) {
extraRows.push(
flexLayout({
items: statItems.slice(i, i + 2),
gap: 210,
direction: "row",
}).join(""),
);
}
const extraItems = `
<svg x="0" y="0"><g transform="translate(-3, ${height - 52 - extraHeight})">
${flexLayout({
items: extraRows,
gap: extraLHeight,
direction: "column",
}).join("")}
</g></svg>
`;
const card = new Card({
defaultTitle: header.length > 35 ? `${header.slice(0, 35)}...` : header,
titlePrefixIcon: icons.contribs,
width: card_width,
height,
border_radius,
colors,
});
card.disableAnimations();
card.setHideBorder(hide_border);
card.setHideTitle(false);
card.setCSS(`
.description { font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2 }
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.textColor} }
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: ${colors.iconColor};
display: block;
}
`);
return card.render(`
${
isTemplate
? // @ts-ignore
getBadgeSVG(
i18n.t("repocard.template"),
colors.textColor,
card_width - 400,
)
: isArchived
? // @ts-ignore
getBadgeSVG(
i18n.t("repocard.archived"),
colors.textColor,
card_width - 400,
)
: ""
}
<text class="description" x="25" y="-5">
${descriptionSvg}
</text>
<g transform="translate(30, ${height - 75 - extraHeight})">
${starAndForkCount}
</g>
${extraItems}
`);
};
export { renderRepoCard };
-75
View File
@@ -1,75 +0,0 @@
type ThemeNames = keyof typeof import("../../themes/index.js");
type RankIcon = "default" | "github" | "percentile";
type CommonOptions = {
title_color: string;
icon_color: string;
text_color: string;
bg_color: string;
theme: ThemeNames;
border_radius: number;
border_color: string;
locale: string;
hide_border: boolean;
};
export type StatCardOptions = CommonOptions & {
hide: string[];
show_icons: boolean;
hide_title: boolean;
card_width: number;
hide_rank: boolean;
include_all_commits: boolean;
commits_year: number;
line_height: number | string;
custom_title: string;
disable_animations: boolean;
number_format: string;
number_precision: number;
ring_color: string;
text_bold: boolean;
rank_icon: RankIcon;
show: string[];
};
export type RepoCardOptions = CommonOptions & {
show_owner: boolean;
description_lines_count: number;
card_width_input;
show: string[];
show_icons: boolean;
number_format: string;
text_bold: boolean;
line_height: number | string;
username;
};
export type TopLangOptions = CommonOptions & {
hide_title: boolean;
card_width: number;
hide: string[];
layout: "compact" | "normal" | "donut" | "donut-vertical" | "pie";
custom_title: string;
langs_count: number;
disable_animations: boolean;
hide_progress: boolean;
prog_bar_bg_color: string;
stats_format: "percentages" | "bytes";
};
export type WakaTimeOptions = CommonOptions & {
hide_title: boolean;
hide: string[];
card_width: number;
line_height: string;
hide_progress: boolean;
custom_title: string;
layout: "compact" | "normal";
langs_count: number;
display_format: "time" | "percent";
disable_animations: boolean;
};
export type GistCardOptions = CommonOptions & {
show_owner: boolean;
};
-275
View File
@@ -1,275 +0,0 @@
// @ts-check
import { encodeHTML } from "./html.js";
import { flexLayout } from "./render.js";
class Card {
/**
* Creates a new card instance.
*
* @param {object} args Card arguments.
* @param {number=} args.width Card width.
* @param {number=} args.height Card height.
* @param {number=} args.border_radius Card border radius.
* @param {string=} args.customTitle Card custom title.
* @param {string=} args.defaultTitle Card default title.
* @param {string=} args.titlePrefixIcon Card title prefix icon.
* @param {object} [args.colors={}] Card colors arguments.
* @param {string=} args.colors.titleColor Card title color.
* @param {string=} args.colors.textColor Card text color.
* @param {string=} args.colors.iconColor Card icon color.
* @param {string|string[]=} args.colors.bgColor Card background color.
* @param {string=} args.colors.borderColor Card border color.
*/
constructor({
width = 100,
height = 100,
border_radius = 4.5,
colors = {},
customTitle,
defaultTitle = "",
titlePrefixIcon,
}) {
this.width = width;
this.height = height;
this.hideBorder = false;
this.hideTitle = false;
this.border_radius = border_radius;
// returns theme based colors with proper overrides and defaults
this.colors = colors;
this.title =
customTitle === undefined
? encodeHTML(defaultTitle)
: encodeHTML(customTitle);
this.css = "";
this.paddingX = 25;
this.paddingY = 35;
this.titlePrefixIcon = titlePrefixIcon;
this.animations = true;
this.a11yTitle = "";
this.a11yDesc = "";
}
/**
* @returns {void}
*/
disableAnimations() {
this.animations = false;
}
/**
* @param {Object} props The props object.
* @param {string} props.title Accessibility title.
* @param {string} props.desc Accessibility description.
* @returns {void}
*/
setAccessibilityLabel({ title, desc }) {
this.a11yTitle = title;
this.a11yDesc = desc;
}
/**
* @param {string} value The CSS to add to the card.
* @returns {void}
*/
setCSS(value) {
this.css = value;
}
/**
* @param {boolean} value Whether to hide the border or not.
* @returns {void}
*/
setHideBorder(value) {
this.hideBorder = value;
}
/**
* @param {boolean} value Whether to hide the title or not.
* @returns {void}
*/
setHideTitle(value) {
this.hideTitle = value;
if (value) {
this.height -= 30;
}
}
/**
* @param {string} text The title to set.
* @returns {void}
*/
setTitle(text) {
this.title = text;
}
/**
* @returns {string} The rendered card title.
*/
renderTitle() {
const titleText = `
<text
x="0"
y="0"
class="header"
data-testid="header"
>${this.title}</text>
`;
const prefixIcon = `
<svg
class="icon"
x="0"
y="-13"
viewBox="0 0 16 16"
version="1.1"
width="16"
height="16"
>
${this.titlePrefixIcon}
</svg>
`;
return `
<g
data-testid="card-title"
transform="translate(${this.paddingX}, ${this.paddingY})"
>
${flexLayout({
items: [this.titlePrefixIcon ? prefixIcon : "", titleText],
gap: 25,
}).join("")}
</g>
`;
}
/**
* @returns {string} The rendered card gradient.
*/
renderGradient() {
if (typeof this.colors.bgColor !== "object") {
return "";
}
const gradients = this.colors.bgColor.slice(1);
return typeof this.colors.bgColor === "object"
? `
<defs>
<linearGradient
id="gradient"
gradientTransform="rotate(${this.colors.bgColor[0]})"
gradientUnits="userSpaceOnUse"
>
${gradients.map((grad, index) => {
let offset = (index * 100) / (gradients.length - 1);
return `<stop offset="${offset}%" stop-color="#${grad}" />`;
})}
</linearGradient>
</defs>
`
: "";
}
/**
* Retrieves css animations for a card.
*
* @returns {string} Animation css.
*/
getAnimations = () => {
return `
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`;
};
/**
* @param {string} body The inner body of the card.
* @returns {string} The rendered card.
*/
render(body) {
return `
<svg
width="${this.width}"
height="${this.height}"
viewBox="0 0 ${this.width} ${this.height}"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId">${this.a11yTitle}</title>
<desc id="descId">${this.a11yDesc}</desc>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: ${this.colors.titleColor};
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
${this.css}
${process.env.NODE_ENV === "test" ? "" : this.getAnimations()}
${
this.animations === false
? `* { animation-duration: 0s !important; animation-delay: 0s !important; }`
: ""
}
</style>
${this.renderGradient()}
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="${this.border_radius}"
height="99%"
stroke="${this.colors.borderColor}"
width="${this.width - 1}"
fill="${
typeof this.colors.bgColor === "object"
? "url(#gradient)"
: this.colors.bgColor
}"
stroke-opacity="${this.hideBorder ? 0 : 1}"
/>
${this.hideTitle ? "" : this.renderTitle()}
<g
data-testid="main-card-body"
transform="translate(0, ${
this.hideTitle ? this.paddingX : this.paddingY + 20
})"
>
${body}
</g>
</svg>
`;
}
}
export { Card };
export default Card;
-42
View File
@@ -1,42 +0,0 @@
// @ts-check
const FALLBACK_LOCALE = "en";
/**
* I18n translation class.
*/
class I18n {
/**
* Constructor.
*
* @param {Object} options Options.
* @param {string=} options.locale Locale.
* @param {any} options.translations Translations.
*/
constructor({ locale, translations }) {
this.locale = locale || FALLBACK_LOCALE;
this.translations = translations;
}
/**
* Get translation.
*
* @param {string} str String to translate.
* @returns {string} Translated string.
*/
t(str) {
if (!this.translations[str]) {
throw new Error(`${str} Translation string not found`);
}
if (!this.translations[str][this.locale]) {
throw new Error(
`'${str}' translation not found for locale '${this.locale}'`,
);
}
return this.translations[str][this.locale];
}
}
export { I18n };
+25 -25
View File
@@ -1,8 +1,11 @@
// @ts-check
import {
getConfig,
renderError,
} from "@stats-organization/github-readme-stats-core";
import { blacklist } from "./blacklist.js";
import { gistWhitelist, whitelist } from "./envs.js";
import { renderError } from "./render.js";
const NOT_WHITELISTED_USERNAME_MESSAGE = "This username is not whitelisted";
const NOT_WHITELISTED_GIST_MESSAGE = "This gist ID is not whitelisted";
@@ -12,36 +15,35 @@ const BLACKLISTED_MESSAGE = "This username is blacklisted";
* Guards access using whitelist/blacklist.
*
* @param {Object} args The parameters object.
* @param {any} args.res The response object.
* @param {string} args.id Resource identifier (username or gist id).
* @param {"username"|"gist"|"wakatime"} args.type The type of identifier.
* @param {{ title_color?: string, text_color?: string, bg_color?: string, border_color?: string, theme?: string }} args.colors Color options for the error card.
* @returns {{ isPassed: boolean, result?: any }} The result object indicating success or failure.
*/
const guardAccess = ({ res, id, type, colors }) => {
const guardAccess = ({ id, type, colors }) => {
if (!["username", "gist", "wakatime"].includes(type)) {
throw new Error(
'Invalid type. Expected "username", "gist", or "wakatime".',
);
}
const currentWhitelist = type === "gist" ? gistWhitelist : whitelist;
const config = getConfig();
const currentWhitelist =
type === "gist" ? config.gistWhitelist : config.whitelist;
const notWhitelistedMsg =
type === "gist"
? NOT_WHITELISTED_GIST_MESSAGE
: NOT_WHITELISTED_USERNAME_MESSAGE;
if (Array.isArray(currentWhitelist) && !currentWhitelist.includes(id)) {
const result = res.send(
renderError({
message: notWhitelistedMsg,
secondaryMessage: "Please deploy your own instance",
renderOptions: {
...colors,
show_repo_link: false,
},
}),
);
const result = renderError({
message: notWhitelistedMsg,
secondaryMessage: "Please deploy your own instance",
renderOptions: {
...colors,
show_repo_link: false,
},
});
return { isPassed: false, result };
}
@@ -50,16 +52,14 @@ const guardAccess = ({ res, id, type, colors }) => {
currentWhitelist === undefined &&
blacklist.includes(id)
) {
const result = res.send(
renderError({
message: BLACKLISTED_MESSAGE,
secondaryMessage: "Please deploy your own instance",
renderOptions: {
...colors,
show_repo_link: false,
},
}),
);
const result = renderError({
message: BLACKLISTED_MESSAGE,
secondaryMessage: "Please deploy your own instance",
renderOptions: {
...colors,
show_repo_link: false,
},
});
return { isPassed: false, result };
}
+3 -6
View File
@@ -1,6 +1,6 @@
// @ts-check
import { clampValue } from "./ops.js";
import { clampValue } from "@stats-organization/github-readme-stats-core";
const MIN = 60;
const HOUR = 60 * MIN;
@@ -106,7 +106,7 @@ const disableCaching = (res) => {
* @param {number} cacheSeconds The cache seconds to set in the headers.
*/
const setCacheHeaders = (res, cacheSeconds) => {
if (cacheSeconds < 1 || process.env.NODE_ENV === "development") {
if (cacheSeconds < 1) {
disableCaching(res);
return;
}
@@ -128,10 +128,7 @@ const setErrorCacheHeaders = (res) => {
const envCacheSeconds = process.env.CACHE_SECONDS
? parseInt(process.env.CACHE_SECONDS, 10)
: NaN;
if (
(!isNaN(envCacheSeconds) && envCacheSeconds < 1) ||
process.env.NODE_ENV === "development"
) {
if (!isNaN(envCacheSeconds) && envCacheSeconds < 1) {
disableCaching(res);
return;
}
-145
View File
@@ -1,145 +0,0 @@
// @ts-check
import { themes } from "../../themes/index.js";
/**
* Checks if a string is a valid hex color.
*
* @param {string} hexColor String to check.
* @returns {boolean} True if the given string is a valid hex color.
*/
const isValidHexColor = (hexColor) => {
return new RegExp(
/^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4})$/,
).test(hexColor);
};
/**
* Check if the given string is a valid gradient.
*
* @param {string[]} colors Array of colors.
* @returns {boolean} True if the given string is a valid gradient.
*/
const isValidGradient = (colors) => {
return (
colors.length > 2 &&
colors.slice(1).every((color) => isValidHexColor(color))
);
};
/**
* Retrieves a gradient if color has more than one valid hex codes else a single color.
*
* @param {string} color The color to parse.
* @param {string | string[]} fallbackColor The fallback color.
* @returns {string | string[]} The gradient or color.
*/
const fallbackColor = (color, fallbackColor) => {
let gradient = null;
let colors = color ? color.split(",") : [];
if (colors.length > 1 && isValidGradient(colors)) {
gradient = colors;
}
return (
(gradient ? gradient : isValidHexColor(color) && `#${color}`) ||
fallbackColor
);
};
/**
* Object containing card colors.
* @typedef {{
* titleColor: string;
* iconColor: string;
* textColor: string;
* bgColor: string | string[];
* borderColor: string;
* ringColor: string;
* }} CardColors
*/
/**
* Returns theme based colors with proper overrides and defaults.
*
* @param {Object} args Function arguments.
* @param {string=} args.title_color Card title color.
* @param {string=} args.text_color Card text color.
* @param {string=} args.icon_color Card icon color.
* @param {string=} args.bg_color Card background color.
* @param {string=} args.border_color Card border color.
* @param {string=} args.ring_color Card ring color.
* @param {string=} args.theme Card theme.
* @returns {CardColors} Card colors.
*/
const getCardColors = ({
title_color,
text_color,
icon_color,
bg_color,
border_color,
ring_color,
theme,
}) => {
const defaultTheme = themes["default"];
const isThemeProvided =
theme !== null && theme !== undefined && theme in themes;
// @ts-ignore
const selectedTheme = isThemeProvided ? themes[theme] : defaultTheme;
const defaultBorderColor =
"border_color" in selectedTheme
? selectedTheme.border_color
: // @ts-ignore
defaultTheme.border_color;
// get the color provided by the user else the theme color
// finally if both colors are invalid fallback to default theme
const titleColor = fallbackColor(
title_color || selectedTheme.title_color,
"#" + defaultTheme.title_color,
);
// get the color provided by the user else the theme color
// finally if both colors are invalid we use the titleColor
const ringColor = fallbackColor(
// @ts-ignore
ring_color || selectedTheme.ring_color,
titleColor,
);
const iconColor = fallbackColor(
icon_color || selectedTheme.icon_color,
"#" + defaultTheme.icon_color,
);
const textColor = fallbackColor(
text_color || selectedTheme.text_color,
"#" + defaultTheme.text_color,
);
const bgColor = fallbackColor(
bg_color || selectedTheme.bg_color,
"#" + defaultTheme.bg_color,
);
const borderColor = fallbackColor(
border_color || defaultBorderColor,
"#" + defaultBorderColor,
);
if (
typeof titleColor !== "string" ||
typeof textColor !== "string" ||
typeof ringColor !== "string" ||
typeof iconColor !== "string" ||
typeof borderColor !== "string"
) {
throw new Error(
"Unexpected behavior, all colors except background should be string.",
);
}
return { titleColor, iconColor, textColor, bgColor, borderColor, ringColor };
};
export { fallbackColor, getCardColors };
-9
View File
@@ -1,9 +0,0 @@
// @ts-check
/**
* Valid owner affiliations for GitHub API queries.
* @type {Array<string>}
*/
const OWNER_AFFILIATIONS = ["OWNER", "COLLABORATOR", "ORGANIZATION_MEMBER"];
export { OWNER_AFFILIATIONS };
+7 -11
View File
@@ -1,14 +1,10 @@
/**
* In the browser this has to be mocked to avoid runtime errors
* @see apps/frontend/vite.config.ts
*/
import { Pool } from "pg";
export const pool = process.env.POSTGRES_URL
? new Pool({
connectionString: process.env.POSTGRES_URL,
})
: null;
export let pool = null;
if (process.env.POSTGRES_URL) {
const { Pool } = await import("pg");
pool = new Pool({
connectionString: process.env.POSTGRES_URL,
});
}
/**
* Creates all required tables if they do not exist.
-15
View File
@@ -1,15 +0,0 @@
// @ts-check
const whitelist = process.env.WHITELIST
? process.env.WHITELIST.split(",")
: undefined;
const gistWhitelist = process.env.GIST_WHITELIST
? process.env.GIST_WHITELIST.split(",")
: undefined;
const excludeRepositories = process.env.EXCLUDE_REPO
? process.env.EXCLUDE_REPO.split(",")
: [];
export { whitelist, gistWhitelist, excludeRepositories };
-91
View File
@@ -1,91 +0,0 @@
// @ts-check
import wrap from "word-wrap";
import { encodeHTML } from "./html.js";
/**
* Retrieves num with suffix k(thousands) precise to given decimal places.
*
* @param {number} num The number to format.
* @param {number=} precision The number of decimal places to include.
* @returns {string|number} The formatted number.
*/
const kFormatter = (num, precision) => {
const abs = Math.abs(num);
const sign = Math.sign(num);
if (typeof precision === "number" && !isNaN(precision)) {
return (sign * (abs / 1000)).toFixed(precision) + "k";
}
if (abs < 1000) {
return sign * abs;
}
return sign * parseFloat((abs / 1000).toFixed(1)) + "k";
};
/**
* Convert bytes to a human-readable string representation.
*
* @param {number} bytes The number of bytes to convert.
* @returns {string} The human-readable representation of bytes.
* @throws {Error} If bytes is negative or too large.
*/
const formatBytes = (bytes) => {
if (bytes < 0) {
throw new Error("Bytes must be a non-negative number");
}
if (bytes === 0) {
return "0 B";
}
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
const base = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(base));
if (i >= sizes.length) {
throw new Error("Bytes is too large to convert to a human-readable string");
}
return `${(bytes / Math.pow(base, i)).toFixed(1)} ${sizes[i]}`;
};
/**
* Split text over multiple lines based on the card width.
*
* @param {string} text Text to split.
* @param {number} width Line width in number of characters.
* @param {number} maxLines Maximum number of lines.
* @returns {string[]} Array of lines.
*/
const wrapTextMultiline = (text, width = 59, maxLines = 3) => {
const fullWidthComma = "";
const encoded = encodeHTML(text);
const isChinese = encoded.includes(fullWidthComma);
let wrapped = [];
if (isChinese) {
wrapped = encoded.split(fullWidthComma); // Chinese full punctuation
} else {
wrapped = wrap(encoded, {
width,
}).split("\n"); // Split wrapped lines to get an array of lines
}
const lines = wrapped.map((line) => line.trim()).slice(0, maxLines); // Only consider maxLines lines
// Add "..." to the last line if the text exceeds maxLines
if (wrapped.length > maxLines) {
lines[maxLines - 1] += "...";
}
// Remove empty lines if text fits in less than maxLines lines
const multiLineText = lines.filter(Boolean);
return multiLineText;
};
export { kFormatter, formatBytes, wrapTextMultiline };
-21
View File
@@ -1,21 +0,0 @@
// @ts-check
import axios from "axios";
/**
* Send GraphQL request to GitHub API.
*
* @param {import('axios').AxiosRequestConfig['data']} data Request data.
* @param {import('axios').AxiosRequestConfig['headers']} headers Request headers.
* @returns {Promise<any>} Request response.
*/
const request = (data, headers) => {
return axios({
url: "https://api.github.com/graphql",
method: "post",
headers,
data,
});
};
export { request };
-13
View File
@@ -1,13 +0,0 @@
// @ts-check
export { blacklist } from "./blacklist.js";
export { Card } from "./Card.js";
export { I18n } from "./I18n.js";
export { icons } from "./icons.js";
export { retryer } from "./retryer.js";
export {
ERROR_CARD_LENGTH,
renderError,
flexLayout,
measureText,
} from "./render.js";
-13
View File
@@ -1,13 +0,0 @@
// @ts-check
const noop = () => {};
/**
* Return console instance based on the environment.
*
* @type {Console | {log: () => void, error: () => void}}
*/
const logger =
process.env.NODE_ENV === "test" ? { log: noop, error: noop } : console;
export { logger };
-239
View File
@@ -1,239 +0,0 @@
// @ts-check
import { getCardColors } from "./color.js";
import { SECONDARY_ERROR_MESSAGES, TRY_AGAIN_LATER } from "./error.js";
import { encodeHTML } from "./html.js";
import { clampValue } from "./ops.js";
/**
* Auto layout utility, allows us to layout things vertically or horizontally with
* proper gaping.
*
* @param {object} props Function properties.
* @param {string[]} props.items Array of items to layout.
* @param {number} props.gap Gap between items.
* @param {"column" | "row"=} props.direction Direction to layout items.
* @param {number[]=} props.sizes Array of sizes for each item.
* @returns {string[]} Array of items with proper layout.
*/
const flexLayout = ({ items, gap, direction, sizes = [] }) => {
let lastSize = 0;
// filter() for filtering out empty strings
return items.filter(Boolean).map((item, i) => {
const size = sizes[i] || 0;
let transform = `translate(${lastSize}, 0)`;
if (direction === "column") {
transform = `translate(0, ${lastSize})`;
}
lastSize += size + gap;
return `<g transform="${transform}">${item}</g>`;
});
};
/**
* Creates a node to display the primary programming language of the repository/gist.
*
* @param {string} langName Language name.
* @param {string} langColor Language color.
* @returns {string} Language display SVG object.
*/
const createLanguageNode = (langName, langColor) => {
return `
<g data-testid="primary-lang">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="${langColor}" />
<text data-testid="lang-name" class="gray" x="15">${langName}</text>
</g>
`;
};
/**
* Create a node to indicate progress in percentage along a horizontal line.
*
* @param {Object} params Object that contains the createProgressNode parameters.
* @param {number} params.x X-axis position.
* @param {number} params.y Y-axis position.
* @param {number} params.width Width of progress bar.
* @param {string} params.color Progress color.
* @param {number} params.progress Progress value.
* @param {string} params.progressBarBackgroundColor Progress bar bg color.
* @param {number} params.delay Delay before animation starts.
* @returns {string} Progress node.
*/
const createProgressNode = ({
x,
y,
width,
color,
progress,
progressBarBackgroundColor,
delay,
}) => {
const progressPercentage = clampValue(progress, 2, 100);
return `
<svg width="${width}" x="${x}" y="${y}">
<rect data-testid="progress-background" rx="5" ry="5" x="0" y="0" width="${width}" height="8" fill="${progressBarBackgroundColor}"></rect>
<svg data-testid="lang-progress" width="${progressPercentage}%">
<rect
height="8"
fill="${color}"
rx="5" ry="5" x="0" y="0"
class="lang-progress"
style="animation-delay: ${delay}ms;"
/>
</svg>
</svg>
`;
};
/**
* Creates an icon with label to display repository/gist stats like forks, stars, etc.
*
* @param {string} icon The icon to display.
* @param {number|string} label The label to display.
* @param {string} testid The testid to assign to the label.
* @param {number} iconSize The size of the icon.
* @returns {string} Icon with label SVG object.
*/
const iconWithLabel = (icon, label, testid, iconSize) => {
if (typeof label === "number" && label <= 0) {
return "";
}
const iconSvg = `
<svg
class="icon"
y="-12"
viewBox="0 0 16 16"
version="1.1"
width="${iconSize}"
height="${iconSize}"
>
${icon}
</svg>
`;
const text = `<text data-testid="${testid}" class="gray">${label}</text>`;
return flexLayout({ items: [iconSvg, text], gap: 20 }).join("");
};
// Script parameters.
const ERROR_CARD_LENGTH = 576.5;
const UPSTREAM_API_ERRORS = [
TRY_AGAIN_LATER,
SECONDARY_ERROR_MESSAGES.MAX_RETRY,
];
/**
* Renders error message on the card.
*
* @param {object} args Function arguments.
* @param {string} args.message Main error message.
* @param {string} [args.secondaryMessage=""] The secondary error message.
* @param {object} [args.renderOptions={}] Render options.
* @param {string=} args.renderOptions.title_color Card title color.
* @param {string=} args.renderOptions.text_color Card text color.
* @param {string=} args.renderOptions.bg_color Card background color.
* @param {string=} args.renderOptions.border_color Card border color.
* @param {Parameters<typeof getCardColors>[0]["theme"]=} args.renderOptions.theme Card theme.
* @param {boolean=} args.renderOptions.show_repo_link Whether to show repo link or not.
* @returns {string} The SVG markup.
*/
const renderError = ({
message,
secondaryMessage = "",
renderOptions = {},
}) => {
const {
title_color,
text_color,
bg_color,
border_color,
theme = "default",
show_repo_link = true,
} = renderOptions;
// returns theme based colors with proper overrides and defaults
const { titleColor, textColor, bgColor, borderColor } = getCardColors({
title_color,
text_color,
icon_color: "",
bg_color,
border_color,
ring_color: "",
theme,
});
return `
<svg width="${ERROR_CARD_LENGTH}" height="120" viewBox="0 0 ${ERROR_CARD_LENGTH} 120" fill="${bgColor}" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${titleColor} }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${textColor} }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="${
ERROR_CARD_LENGTH - 1
}" height="99%" rx="4.5" fill="${bgColor}" stroke="${borderColor}"/>
<text x="25" y="45" class="text">Something went wrong!${
UPSTREAM_API_ERRORS.includes(secondaryMessage) || !show_repo_link
? ""
: " file an issue at https://tinyurl.com/github-stats"
}</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">${encodeHTML(message)}</tspan>
<tspan x="25" dy="18" class="gray">${secondaryMessage}</tspan>
</text>
</svg>
`;
};
/**
* Retrieve text length.
*
* @see https://stackoverflow.com/a/48172630/10629172
* @param {string} str String to measure.
* @param {number} fontSize Font size.
* @returns {number} Text length.
*/
const measureText = (str, fontSize = 10) => {
// prettier-ignore
const widths = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0.2796875, 0.2765625,
0.3546875, 0.5546875, 0.5546875, 0.8890625, 0.665625, 0.190625,
0.3328125, 0.3328125, 0.3890625, 0.5828125, 0.2765625, 0.3328125,
0.2765625, 0.3015625, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875, 0.5546875,
0.2765625, 0.2765625, 0.584375, 0.5828125, 0.584375, 0.5546875,
1.0140625, 0.665625, 0.665625, 0.721875, 0.721875, 0.665625,
0.609375, 0.7765625, 0.721875, 0.2765625, 0.5, 0.665625,
0.5546875, 0.8328125, 0.721875, 0.7765625, 0.665625, 0.7765625,
0.721875, 0.665625, 0.609375, 0.721875, 0.665625, 0.94375,
0.665625, 0.665625, 0.609375, 0.2765625, 0.3546875, 0.2765625,
0.4765625, 0.5546875, 0.3328125, 0.5546875, 0.5546875, 0.5,
0.5546875, 0.5546875, 0.2765625, 0.5546875, 0.5546875, 0.221875,
0.240625, 0.5, 0.221875, 0.8328125, 0.5546875, 0.5546875,
0.5546875, 0.5546875, 0.3328125, 0.5, 0.2765625, 0.5546875,
0.5, 0.721875, 0.5, 0.5, 0.5, 0.3546875, 0.259375, 0.353125, 0.5890625,
];
const avg = 0.5279276315789471;
return (
str
.split("")
.map((c) =>
c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg,
)
.reduce((cur, acc) => acc + cur) * fontSize
);
};
export {
ERROR_CARD_LENGTH,
renderError,
createLanguageNode,
createProgressNode,
iconWithLabel,
flexLayout,
measureText,
};
-113
View File
@@ -1,113 +0,0 @@
// @ts-check
import { getUserAccessByName } from "./database.js";
import { CustomError } from "./error.js";
import { logger } from "./log.js";
/**
* Returns a random integer from 0 (inclusive) to `max` (exclusive).
*
* The value is generated using `Math.random()` and uniformly distributed
* across the range.
*
* @param {number} max The upper bound (exclusive). Must be a positive number.
*
* @returns {number} A random integer `n` such that `0 <= n < max`.
*/
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
/**
* @typedef {import("axios").AxiosResponse} AxiosResponse Axios response.
* @typedef {(variables: any, token: string, retriesForTests?: number) => Promise<AxiosResponse>} FetcherFunction Fetcher function.
*/
/**
* Try to execute the fetcher function until it succeeds or the max number of retries is reached.
*
* @param {FetcherFunction} fetcher The fetcher function.
* @param {string?} username GitHub username of the user whose PAT to use, if available
* @param {any} variables Object with arguments to pass to the fetcher function.
* @returns {Promise<any>} The response from the fetcher function.
*/
const retryer = async (fetcher, username, variables) => {
let userPAT;
if (username) {
userPAT = await getUserAccessByName(username);
}
let PATs;
if (userPAT?.token) {
PATs = [{ name: `USER_${username}`, value: userPAT.token }];
} else {
const patNames = Object.keys(process.env).filter((key) =>
/PAT_\d*$/.exec(key),
);
PATs = patNames.map((name) => ({ name, value: process.env[name] }));
}
if (!PATs.length) {
throw new CustomError("No GitHub API tokens found", CustomError.NO_TOKENS);
}
const startPAT = getRandomInt(PATs.length);
for (let retries = 0; retries < PATs.length; retries++) {
const currentPAT = PATs[(startPAT + retries) % PATs.length];
try {
let response = await fetcher(
variables,
// @ts-ignore
currentPAT.value,
// used in tests for faking rate limit
retries,
);
// react on both type and message-based rate-limit signals.
// https://github.com/anuraghazra/github-readme-stats/issues/4425
const errors = response?.data?.errors;
const errorType = errors?.[0]?.type;
const errorMsg = errors?.[0]?.message || "";
const isRateLimited =
(errors && errorType === "RATE_LIMITED") ||
/rate limit/i.test(errorMsg);
if (isRateLimited) {
logger.log(`${currentPAT.name} Failed due to rate limiting`);
} else {
return response;
}
} catch (err) {
/** @type {any} */
const e = err;
// network/unexpected error → let caller treat as failure
if (!e?.response) {
throw e;
}
// prettier-ignore
// also checking for bad credentials if any tokens gets invalidated
const isBadCredential =
e?.response?.data?.message === "Bad credentials";
const isAccountSuspended =
e?.response?.data?.message === "Sorry. Your account was suspended.";
if (isBadCredential || isAccountSuspended) {
logger.log(`${currentPAT.name} Failed due to bad credentials`);
} else {
// HTTP error with a response → return it for caller-side handling
return e.response;
}
}
}
throw new CustomError(
"Downtime due to GitHub API rate limiting",
CustomError.MAX_RETRY,
);
};
export { retryer };
export default retryer;
-113
View File
@@ -1,113 +0,0 @@
// @ts-check
import { MissingParamError } from "../common/error.js";
import { request } from "../common/http.js";
import { retryer } from "../common/retryer.js";
const QUERY = `
query gistInfo($gistName: String!) {
viewer {
gist(name: $gistName) {
description
owner {
login
}
stargazerCount
forks {
totalCount
}
files {
name
language {
name
}
size
}
}
}
}
`;
/**
* Gist data fetcher.
*
* @param {object} variables Fetcher variables.
* @param {string} token GitHub token.
* @returns {Promise<import('axios').AxiosResponse>} The response.
*/
const fetcher = async (variables, token) => {
return await request(
{ query: QUERY, variables },
{ Authorization: `token ${token}` },
);
};
/**
* @typedef {{ name: string; language: { name: string; }, size: number }} GistFile Gist file.
*/
/**
* This function calculates the primary language of a gist by files size.
*
* @param {GistFile[]} files Files.
* @returns {string} Primary language.
*/
const calculatePrimaryLanguage = (files) => {
/** @type {Record<string, number>} */
const languages = {};
for (const file of files) {
if (file.language) {
if (languages[file.language.name]) {
languages[file.language.name] += file.size;
} else {
languages[file.language.name] = file.size;
}
}
}
let primaryLanguage = Object.keys(languages)[0];
for (const language in languages) {
if (languages[language] > languages[primaryLanguage]) {
primaryLanguage = language;
}
}
return primaryLanguage;
};
/**
* @typedef {import('./types').GistData} GistData Gist data.
*/
/**
* Fetch GitHub gist information by given username and ID.
*
* @param {string} id GitHub gist ID.
* @returns {Promise<GistData>} Gist data.
*/
const fetchGist = async (id) => {
if (!id) {
throw new MissingParamError(["id"], "/api/gist?id=GIST_ID");
}
const res = await retryer(fetcher, null, { gistName: id });
if (res.data.errors) {
throw new Error(res.data.errors[0].message);
}
if (!res.data.data.viewer.gist) {
throw new Error("Gist not found");
}
const data = res.data.data.viewer.gist;
return {
name: data.files[Object.keys(data.files)[0]].name,
nameWithOwner: `${data.owner.login}/${
data.files[Object.keys(data.files)[0]].name
}`,
description: data.description,
language: calculatePrimaryLanguage(data.files),
starsCount: data.stargazerCount,
forksCount: data.forks.totalCount,
};
};
export { fetchGist };
-162
View File
@@ -1,162 +0,0 @@
// @ts-check
import { MissingParamError } from "../common/error.js";
import { request } from "../common/http.js";
import { retryer } from "../common/retryer.js";
import { fetchRepoUserStats } from "./stats.js";
/**
* Repo data fetcher.
*
* @param {object} variables Fetcher variables.
* @param {string} token GitHub token.
* @returns {Promise<import('axios').AxiosResponse>} The response.
*/
const fetcher = (variables, token) => {
return request(
{
query: `
fragment RepoInfo on Repository {
name
nameWithOwner
isPrivate
isArchived
isTemplate
stargazers {
totalCount
}
description
primaryLanguage {
color
id
name
}
forkCount
}
query getRepo($login: String!, $repo: String!) {
user(login: $login) {
repository(name: $repo) {
...RepoInfo
}
}
organization(login: $login) {
repository(name: $repo) {
...RepoInfo
}
}
}
`,
variables,
},
{
Authorization: `token ${token}`,
},
);
};
const urlExample = "/api/pin?username=USERNAME&amp;repo=REPO_NAME";
/**
* @typedef {import("./types").RepositoryData} RepositoryData Repository data.
*/
/**
* Fetch repository data.
*
* @param {string} username GitHub username.
* @param {string} reponame GitHub repository name.
* @returns {Promise<RepositoryData>} Repository data.
*/
const fetchRepo = async (
username,
reponame,
include_prs_authored = false,
include_prs_commented = false,
include_prs_reviewed = false,
include_issues_authored = false,
include_issues_commented = false,
) => {
let owner = username;
if (reponame && reponame.includes("/")) {
const [parsed_owner, parsed_repo] = reponame.split("/");
owner = parsed_owner;
reponame = parsed_repo;
}
if (owner && !username) {
username = owner;
}
if (username && !owner) {
owner = username;
}
if (!username && !reponame) {
throw new MissingParamError(["username", "repo"], urlExample);
}
if (!username) {
throw new MissingParamError(["username"], urlExample);
}
if (!reponame) {
throw new MissingParamError(["repo"], urlExample);
}
let res = await retryer(fetcher, username, { login: owner, repo: reponame });
const data = res.data.data;
if (!data.user && !data.organization) {
throw new Error("Not found");
}
const isUser = data.organization === null && data.user;
const isOrg = data.user === null && data.organization;
if (isUser) {
if (!data.user.repository || data.user.repository.isPrivate) {
throw new Error("User Repository Not found");
}
let repoUserStats = await fetchRepoUserStats(
username,
[owner + "/" + reponame],
[],
include_prs_authored,
include_prs_commented,
include_prs_reviewed,
include_issues_authored,
include_issues_commented,
);
return {
...repoUserStats,
...data.user.repository,
starCount: data.user.repository.stargazers.totalCount,
};
}
if (isOrg) {
if (
!data.organization.repository ||
data.organization.repository.isPrivate
) {
throw new Error("Organization Repository Not found");
}
let repoUserStats = await fetchRepoUserStats(
username,
[owner + "/" + reponame],
[],
include_prs_authored,
include_prs_commented,
include_prs_reviewed,
include_issues_authored,
include_issues_commented,
);
return {
...repoUserStats,
...data.organization.repository,
starCount: data.organization.repository.stargazers.totalCount,
};
}
throw new Error("Unexpected behavior");
};
export { fetchRepo };
-457
View File
@@ -1,457 +0,0 @@
// @ts-check
import axios from "axios";
import githubUsernameRegex from "github-username-regex";
import { calculateRank } from "../calculateRank.js";
import { excludeRepositories } from "../common/envs.js";
import { CustomError, MissingParamError } from "../common/error.js";
import { wrapTextMultiline } from "../common/fmt.js";
import { request } from "../common/http.js";
import { logger } from "../common/log.js";
import { buildSearchFilter, parseOwnerAffiliations } from "../common/ops.js";
import { retryer } from "../common/retryer.js";
// GraphQL queries.
const GRAPHQL_REPOS_FIELD = `
repositories(first: 100, after: $after, ownerAffiliations: $ownerAffiliations, orderBy: {direction: DESC, field: STARGAZERS}) {
totalCount
nodes {
name
stargazers {
totalCount
}
}
pageInfo {
hasNextPage
endCursor
}
}
`;
const GRAPHQL_REPOS_QUERY = `
query userInfo($login: String!, $after: String, $ownerAffiliations: [RepositoryAffiliation]) {
user(login: $login) {
${GRAPHQL_REPOS_FIELD}
}
}
`;
const GRAPHQL_STATS_QUERY = `
query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {
user(login: $login) {
name
login
commits: contributionsCollection (from: $startTime) {
totalCommitContributions,
}
reviews: contributionsCollection {
totalPullRequestReviewContributions
}
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
totalCount
}
pullRequests(first: 1) {
totalCount
}
mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {
totalCount
}
openIssues: issues(states: OPEN) {
totalCount
}
closedIssues: issues(states: CLOSED) {
totalCount
}
followers {
totalCount
}
repositoryDiscussions @include(if: $includeDiscussions) {
totalCount
}
repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {
totalCount
}
${GRAPHQL_REPOS_FIELD}
}
}
`;
/**
* Stats fetcher object.
*
* @param {object & { after: string | null }} variables Fetcher variables.
* @param {string} token GitHub token.
* @returns {Promise<import('axios').AxiosResponse>} Axios response.
*/
const fetcher = (variables, token) => {
const query = variables.after ? GRAPHQL_REPOS_QUERY : GRAPHQL_STATS_QUERY;
return request(
{
query,
variables,
},
{
Authorization: `bearer ${token}`,
},
);
};
/**
* Fetch stats information for a given username.
*
* @param {object} variables Fetcher variables.
* @param {string} variables.username GitHub username.
* @param {boolean} variables.includeMergedPullRequests Include merged pull requests.
* @param {boolean} variables.includeDiscussions Include discussions.
* @param {boolean} variables.includeDiscussionsAnswers Include discussions answers.
* @param {string|undefined} variables.startTime Time to start the count of total commits.
* @param {string[]} ownerAffiliations The owner affiliations to filter by. Default: OWNER.
* @returns {Promise<import('axios').AxiosResponse>} Axios response.
*
* @description This function supports multi-page fetching if the 'FETCH_MULTI_PAGE_STARS' environment variable is set to true or a limit of fetches.
*/
const statsFetcher = async ({
username,
includeMergedPullRequests,
includeDiscussions,
includeDiscussionsAnswers,
startTime,
ownerAffiliations,
}) => {
let stats;
let hasNextPage = true;
let endCursor = null;
let fetchedPages = 0;
while (hasNextPage) {
const variables = {
login: username,
first: 100,
after: endCursor,
includeMergedPullRequests,
includeDiscussions,
includeDiscussionsAnswers,
startTime,
ownerAffiliations,
};
let res = await retryer(fetcher, username, variables);
if (res.data.errors) {
return res;
}
// Store stats data.
const repoNodes = res.data.data.user.repositories.nodes;
if (stats) {
if (fetchedPages === 1) {
// make deep copy of relevant stats fields to avoid altering the cached response object in frontend
stats = structuredClone({
data: stats.data,
statusText: stats.statusText,
});
}
stats.data.data.user.repositories.nodes.push(...repoNodes);
} else {
stats = res;
}
// Disable multi page fetching on public Vercel instance due to rate limits.
fetchedPages++;
const repoNodesWithStars = repoNodes.filter(
(node) => node.stargazers.totalCount !== 0,
);
hasNextPage =
(process.env.FETCH_MULTI_PAGE_STARS === "true" ||
process.env.FETCH_MULTI_PAGE_STARS > fetchedPages) &&
repoNodes.length === repoNodesWithStars.length &&
res.data.data.user.repositories.pageInfo.hasNextPage;
endCursor = res.data.data.user.repositories.pageInfo.endCursor;
}
return stats;
};
/**
* Fetch total commits using the REST API.
*
* @param {object} variables Fetcher variables.
* @param {string} token GitHub token.
* @returns {Promise<import('axios').AxiosResponse>} Axios response.
*
* @see https://developer.github.com/v3/search/#search-commits
*/
const fetchTotalItems = (variables, token) => {
return axios({
method: "get",
url:
`https://api.github.com/search/` +
variables.type +
`?per_page=1&q=` +
buildSearchFilter(variables.repo, variables.owner).replaceAll(" ", "+") +
variables.filter,
headers: {
"Content-Type": "application/json",
Accept: "application/vnd.github.cloak-preview",
Authorization: `token ${token}`,
},
});
};
/**
* Fetch all the commits for all the repositories of a given username.
*
* @param {string} username GitHub username.
* @returns {Promise<number>} Total commits.
*
* @description Done like this because the GitHub API does not provide a way to fetch all the commits. See
* #92#issuecomment-661026467 and #211 for more information.
*/
const totalItemsFetcher = async (username, repo, owner, type, filter) => {
if (!githubUsernameRegex.test(username)) {
logger.log("Invalid username provided.");
throw new Error("Invalid username provided.");
}
let res;
try {
res = await retryer(fetchTotalItems, username, {
login: username,
repo,
owner,
type,
filter,
});
} catch (err) {
logger.log(err);
throw new Error(err);
}
const totalCount = res.data.total_count;
if (isNaN(totalCount)) {
logger.error("GitHub error: " + JSON.stringify(res.data));
throw new CustomError(
"Could not fetch data from GitHub REST API.",
CustomError.GITHUB_REST_API_ERROR,
);
}
return totalCount;
};
const fetchRepoUserStats = async (
username,
repo,
owner,
include_prs_authored,
include_prs_commented,
include_prs_reviewed,
include_issues_authored,
include_issues_commented,
) => {
let stats = {};
if (include_prs_authored) {
stats.totalPRsAuthored = await totalItemsFetcher(
username,
repo,
owner,
"issues",
`author:${username}+type:pr`,
);
}
if (include_prs_commented) {
stats.totalPRsCommented = await totalItemsFetcher(
username,
repo,
owner,
"issues",
`commenter:${username}+-author:${username}+type:pr`,
);
}
if (include_prs_reviewed) {
stats.totalPRsReviewed = await totalItemsFetcher(
username,
repo,
owner,
"issues",
`reviewed-by:${username}+-author:${username}+type:pr`,
);
}
if (include_issues_authored) {
stats.totalIssuesAuthored = await totalItemsFetcher(
username,
repo,
owner,
"issues",
`author:${username}+type:issue`,
);
}
if (include_issues_commented) {
stats.totalIssuesCommented = await totalItemsFetcher(
username,
repo,
owner,
"issues",
`commenter:${username}+-author:${username}+type:issue`,
);
}
return stats;
};
/**
* Fetch stats for a given username.
*
* @param {string} username GitHub username.
* @param {boolean} include_all_commits Include all commits.
* @param {string[]} exclude_repo Repositories to exclude.
* @param {boolean} include_merged_pull_requests Include merged pull requests.
* @param {boolean} include_discussions Include discussions.
* @param {boolean} include_discussions_answers Include discussions answers.
* @param {number|undefined} commits_year Year to count total commits
* @param {string[]} ownerAffiliations Owner affiliations. Default: OWNER.
* @returns {Promise<import("./types").StatsData>} Stats data.
*/
const fetchStats = async (
username,
include_all_commits = false,
exclude_repo = [],
include_merged_pull_requests = false,
include_discussions = false,
include_discussions_answers = false,
commits_year,
repo = [],
owner = [],
include_prs_authored = false,
include_prs_commented = false,
include_prs_reviewed = false,
include_issues_authored = false,
include_issues_commented = false,
ownerAffiliations = [],
) => {
if (!username) {
throw new MissingParamError(["username"]);
}
const stats = {
name: "",
totalPRs: 0,
totalPRsMerged: 0,
mergedPRsPercentage: 0,
totalReviews: 0,
totalCommits: 0,
totalIssues: 0,
totalStars: 0,
totalDiscussionsStarted: 0,
totalDiscussionsAnswered: 0,
contributedTo: 0,
totalPRsAuthored: 0,
totalPRsCommented: 0,
totalPRsReviewed: 0,
totalIssuesAuthored: 0,
totalIssuesCommented: 0,
rank: { level: "C", percentile: 100 },
};
ownerAffiliations = parseOwnerAffiliations(ownerAffiliations);
let res = await statsFetcher({
username,
includeMergedPullRequests: include_merged_pull_requests,
includeDiscussions: include_discussions,
includeDiscussionsAnswers: include_discussions_answers,
startTime: commits_year ? `${commits_year}-01-01T00:00:00Z` : undefined,
ownerAffiliations,
});
// Catch GraphQL errors.
if (res.data.errors) {
logger.error(res.data.errors);
if (res.data.errors[0].type === "NOT_FOUND") {
throw new CustomError(
res.data.errors[0].message || "Could not fetch user.",
CustomError.USER_NOT_FOUND,
);
}
if (res.data.errors[0].message) {
throw new CustomError(
wrapTextMultiline(res.data.errors[0].message, 90, 1)[0],
res.statusText,
);
}
throw new CustomError(
"Something went wrong while trying to retrieve the stats data using the GraphQL API.",
CustomError.GRAPHQL_ERROR,
);
}
const user = res.data.data.user;
stats.name = user.name || user.login;
// if include_all_commits, fetch all commits using the REST API.
if (include_all_commits) {
stats.totalCommits = await totalItemsFetcher(
username,
repo,
owner,
"commits",
`author:${username}`,
);
} else {
stats.totalCommits = user.commits.totalCommitContributions;
}
let repoUserStats = await fetchRepoUserStats(
username,
repo,
owner,
include_prs_authored,
include_prs_commented,
include_prs_reviewed,
include_issues_authored,
include_issues_commented,
);
Object.assign(stats, repoUserStats);
stats.totalPRs = user.pullRequests.totalCount;
if (include_merged_pull_requests) {
stats.totalPRsMerged = user.mergedPullRequests.totalCount;
stats.mergedPRsPercentage =
(user.mergedPullRequests.totalCount / user.pullRequests.totalCount) *
100 || 0;
}
stats.totalReviews = user.reviews.totalPullRequestReviewContributions;
stats.totalIssues = user.openIssues.totalCount + user.closedIssues.totalCount;
if (include_discussions) {
stats.totalDiscussionsStarted = user.repositoryDiscussions.totalCount;
}
if (include_discussions_answers) {
stats.totalDiscussionsAnswered =
user.repositoryDiscussionComments.totalCount;
}
stats.contributedTo = user.repositoriesContributedTo.totalCount;
// Retrieve stars while filtering out repositories to be hidden.
const allExcludedRepos = [...exclude_repo, ...excludeRepositories];
let repoToHide = new Set(allExcludedRepos);
stats.totalStars = user.repositories.nodes
.filter((data) => {
return !repoToHide.has(data.name);
})
.reduce((prev, curr) => {
return prev + curr.stargazers.totalCount;
}, 0);
stats.rank = calculateRank({
all_commits: include_all_commits,
commits: stats.totalCommits,
prs: stats.totalPRs,
reviews: stats.totalReviews,
issues: stats.totalIssues,
repos: user.repositories.totalCount,
stars: stats.totalStars,
followers: user.followers.totalCount,
});
return stats;
};
export { fetchStats, fetchRepoUserStats };
-168
View File
@@ -1,168 +0,0 @@
// @ts-check
import { excludeRepositories } from "../common/envs.js";
import { CustomError, MissingParamError } from "../common/error.js";
import { wrapTextMultiline } from "../common/fmt.js";
import { request } from "../common/http.js";
import { logger } from "../common/log.js";
import { parseOwnerAffiliations } from "../common/ops.js";
import { retryer } from "../common/retryer.js";
/**
* Top languages fetcher object.
*
* @param {any} variables Fetcher variables.
* @param {string} token GitHub token.
* @returns {Promise<import("axios").AxiosResponse>} Languages fetcher response.
*/
const fetcher = (variables, token) => {
return request(
{
query: `
query userInfo($login: String!, $ownerAffiliations: [RepositoryAffiliation]) {
user(login: $login) {
# do not fetch forks
repositories(ownerAffiliations: $ownerAffiliations, isFork: false, first: 100) {
nodes {
name
languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
edges {
size
node {
color
name
}
}
}
}
}
}
}
`,
variables,
},
{
Authorization: `token ${token}`,
},
);
};
/**
* @typedef {import("./types").TopLangData} TopLangData Top languages data.
*/
/**
* Fetch top languages for a given username.
*
* @param {string} username GitHub username.
* @param {string[]} exclude_repo List of repositories to exclude. Default: [].
* @param {number} size_weight Weightage to be given to size.
* @param {number} count_weight Weightage to be given to count.
* @param {string[]} ownerAffiliations The owner affiliations to filter by. Default: OWNER.
* @returns {Promise<TopLangData>} Top languages data.
*/
const fetchTopLanguages = async (
username,
exclude_repo = [],
size_weight = 1,
count_weight = 0,
ownerAffiliations = [],
) => {
if (!username) {
throw new MissingParamError(["username"]);
}
ownerAffiliations = parseOwnerAffiliations(ownerAffiliations);
const res = await retryer(fetcher, username, {
login: username,
ownerAffiliations,
});
if (res.data.errors) {
logger.error(res.data.errors);
if (res.data.errors[0].type === "NOT_FOUND") {
throw new CustomError(
res.data.errors[0].message || "Could not fetch user.",
CustomError.USER_NOT_FOUND,
);
}
if (res.data.errors[0].message) {
throw new CustomError(
wrapTextMultiline(res.data.errors[0].message, 90, 1)[0],
res.statusText,
);
}
throw new CustomError(
"Something went wrong while trying to retrieve the language data using the GraphQL API.",
CustomError.GRAPHQL_ERROR,
);
}
let repoNodes = res.data.data.user.repositories.nodes;
/** @type {Record<string, boolean>} */
let repoToHide = {};
const allExcludedRepos = [...exclude_repo, ...excludeRepositories];
// populate repoToHide map for quick lookup
// while filtering out
if (allExcludedRepos) {
allExcludedRepos.forEach((repoName) => {
repoToHide[repoName] = true;
});
}
// filter out repositories to be hidden
repoNodes = repoNodes
.sort((a, b) => b.size - a.size)
.filter((name) => !repoToHide[name.name]);
let repoCount = 0;
repoNodes = repoNodes
.filter((node) => node.languages.edges.length > 0)
// flatten the list of language nodes
.reduce((acc, curr) => curr.languages.edges.concat(acc), [])
.reduce((acc, prev) => {
// get the size of the language (bytes)
let langSize = prev.size;
// if we already have the language in the accumulator
// & the current language name is same as previous name
// add the size to the language size and increase repoCount.
if (acc[prev.node.name] && prev.node.name === acc[prev.node.name].name) {
langSize = prev.size + acc[prev.node.name].size;
repoCount += 1;
} else {
// reset repoCount to 1
// language must exist in at least one repo to be detected
repoCount = 1;
}
return {
...acc,
[prev.node.name]: {
name: prev.node.name,
color: prev.node.color,
size: langSize,
count: repoCount,
},
};
}, {});
Object.keys(repoNodes).forEach((name) => {
// comparison index calculation
repoNodes[name].size =
Math.pow(repoNodes[name].size, size_weight) *
Math.pow(repoNodes[name].count, count_weight);
});
const topLangs = Object.keys(repoNodes)
.sort((a, b) => repoNodes[b].size - repoNodes[a].size)
.reduce((result, key) => {
result[key] = repoNodes[key];
return result;
}, {});
return topLangs;
};
export { fetchTopLanguages };
-2
View File
@@ -1,2 +0,0 @@
export * from "./common/index.js";
export * from "./cards/index.js";
+1 -1
View File
@@ -66,6 +66,6 @@ export async function repeatRecentRequests() {
if (urls.length === 0) {
console.log("No recent requests found.");
} else {
await makeRequests(urls, 5);
await makeRequests(urls, 3);
}
}
+1
View File
@@ -80,6 +80,7 @@ async function githubAuthenticate(code, privateAccess) {
return { userId, accessToken, needDowngrade };
} catch (err) {
if (err.response) {
// eslint-disable-next-line preserve-caught-error
throw new Error(`OAuth Error: ${err.response.status}`);
}
throw err;
@@ -1,115 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test /api/wakatime > should render error if user data is not accessible 1`] = `
"
<svg
width="495"
height="150"
viewBox="0 0 495 150"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-labelledby="descId"
>
<title id="titleId"></title>
<desc id="descId"></desc>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #2f80ed;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #434d58;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
@keyframes slideInAnimation {
from {
width: 0;
}
to {
width: calc(100%-100px);
}
}
@keyframes growWidthAnimation {
from {
width: 0;
}
to {
width: 100%;
}
}
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
#rect-mask rect{
animation: slideInAnimation 1s ease-in-out forwards;
}
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
}
</style>
<rect
data-testid="card-bg"
x="0.5"
y="0.5"
rx="4.5"
height="99%"
stroke="#e4e2e2"
width="494"
fill="#fffefe"
stroke-opacity="1"
/>
<g
data-testid="card-title"
transform="translate(25, 35)"
>
<g transform="translate(0, 0)">
<text
x="0"
y="0"
class="header"
data-testid="header"
>WakaTime Stats</text>
</g>
</g>
<g
data-testid="main-card-body"
transform="translate(0, 55)"
>
<svg x="0" y="0" width="100%">
<g transform="translate(0, 0)">
<text x="25" y="11" class="stat bold" fill="#434d58">WakaTime user profile not public</text>
</g>
</svg>
</g>
</svg>
"
`;
@@ -1,2 +0,0 @@
process.env.GIST_WHITELIST = "bbfce31e0217a3689c8d961a356cb10d";
process.env.WHITELIST = "anuraghazra";
+133 -297
View File
@@ -1,345 +1,181 @@
// @ts-check
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { api, getConfig } from "@stats-organization/github-readme-stats-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import api from "../api-renamed/index.js";
import { calculateRank } from "../src/calculateRank.js";
import { renderStatsCard } from "../src/cards/stats.js";
import router from "../router.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
import { renderError } from "../src/common/render.js";
import { getUserAccessByName, storeRequest } from "../src/common/database.js";
import { data_stats, stats } from "./test-data/api-data.js";
stats.rank = calculateRank({
all_commits: false,
commits: stats.totalCommits,
prs: stats.totalPRs,
reviews: stats.totalReviews,
issues: stats.totalIssues,
repos: 1,
stars: stats.totalStars,
followers: 0,
vi.mock(import("@stats-organization/github-readme-stats-core"), async () => {
const { mockCore } = await import("./utils.js");
return mockCore();
});
const error = {
errors: [
{
type: "NOT_FOUND",
path: ["user"],
locations: [],
message: "Could not fetch user",
},
],
};
vi.mock(import("../src/common/database.js"), async (importOriginal) => ({
...(await importOriginal()),
storeRequest: vi.fn(),
getUserAccessByName: vi.fn(),
}));
const mock = new MockAdapter(axios);
const apiMock = vi.mocked(api);
const getConfigMock = vi.mocked(getConfig);
const storeRequestMock = vi.mocked(storeRequest);
const getUserAccessByNameMock = vi.mocked(getUserAccessByName);
// @ts-ignore
const faker = (query, data) => {
const req = {
query: {
username: "anuraghazra",
...query,
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
const createRequest = (search = "") => ({
headers: {},
url: `/api?${search}`,
});
return { req, res };
};
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
const defaultCacheHeader =
`max-age=${CACHE_TTL.STATS_CARD.DEFAULT}, ` +
`s-maxage=${CACHE_TTL.STATS_CARD.DEFAULT}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;
const errorCacheHeader =
`max-age=${CACHE_TTL.ERROR}, ` +
`s-maxage=${CACHE_TTL.ERROR}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;
beforeEach(() => {
process.env.CACHE_SECONDS = undefined;
apiMock.mockReset();
getConfigMock.mockReset().mockReturnValue({});
storeRequestMock.mockReset().mockResolvedValue(undefined);
getUserAccessByNameMock.mockReset().mockResolvedValue(null);
// CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally
delete process.env.CACHE_SECONDS;
});
afterEach(() => {
mock.reset();
});
describe("Test /api backend routing", () => {
it("happy path should pass query params and user PAT, respond with stats content and persist request", async () => {
getUserAccessByNameMock.mockResolvedValue({ token: "user-pat" });
apiMock.mockResolvedValue({
status: "success",
content: "mock-stats-svg",
});
describe("Test /api/", () => {
it("should test the request", async () => {
const { req, res } = faker({}, data_stats);
await api(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderStatsCard(stats, { ...req.query }),
const req = createRequest(
"username=anuraghazra&theme=dark&hide=issues,prs,contribs",
);
});
const res = createResponse();
it("should render error card on error", async () => {
const { req, res } = faker({}, error);
await router(req, res);
await api(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: error.errors[0].message,
secondaryMessage:
"Make sure the provided username is not an organization",
}),
);
});
it("should render error card in same theme as requested card", async () => {
const { req, res } = faker({ theme: "merko" }, error);
await api(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: error.errors[0].message,
secondaryMessage:
"Make sure the provided username is not an organization",
renderOptions: { theme: "merko" },
}),
);
});
it("should get the query options", async () => {
const { req, res } = faker(
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
expect(apiMock).toHaveBeenCalledWith(
{
username: "anuraghazra",
theme: "dark",
hide: "issues,prs,contribs",
show_icons: true,
hide_border: true,
line_height: 100,
title_color: "fff",
icon_color: "fff",
text_color: "fff",
bg_color: "fff",
},
data_stats,
"user-pat",
);
await api(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderStatsCard(stats, {
hide: ["issues", "prs", "contribs"],
show_icons: true,
hide_border: true,
line_height: 100,
title_color: "fff",
icon_color: "fff",
text_color: "fff",
bg_color: "fff",
}),
);
});
it("should have proper cache", async () => {
const { req, res } = faker({}, data_stats);
await api(req, res);
expect(req.query).toEqual({
username: "anuraghazra",
theme: "dark",
hide: "issues,prs,contribs",
});
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
[
"Cache-Control",
`max-age=${CACHE_TTL.STATS_CARD.DEFAULT}, ` +
`s-maxage=${CACHE_TTL.STATS_CARD.DEFAULT}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-stats-svg");
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
});
it("should set proper cache", async () => {
const cache_seconds = DURATIONS.TWELVE_HOURS;
const { req, res } = faker({ cache_seconds }, data_stats);
await api(req, res);
it("should use the shorter error cache for temporary stats errors", async () => {
apiMock.mockResolvedValue({
status: "error - temporary",
content: "temporary-error-svg",
});
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
[
"Cache-Control",
`max-age=${cache_seconds}, ` +
`s-maxage=${cache_seconds}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
],
]);
});
const req = createRequest("username=anuraghazra");
const res = createResponse();
it("should set shorter cache when error", async () => {
const { req, res } = faker({}, error);
await api(req, res);
await router(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
[
"Cache-Control",
`max-age=${CACHE_TTL.ERROR}, ` +
`s-maxage=${CACHE_TTL.ERROR}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
],
]);
});
it("should properly set cache using CACHE_SECONDS env variable", async () => {
const cacheSeconds = "10000";
process.env.CACHE_SECONDS = cacheSeconds;
const { req, res } = faker({}, data_stats);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
[
"Cache-Control",
`max-age=${cacheSeconds}, ` +
`s-maxage=${cacheSeconds}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
],
]);
});
it("should disable cache when CACHE_SECONDS is set to 0", async () => {
process.env.CACHE_SECONDS = "0";
const { req, res } = faker({}, data_stats);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
[
"Cache-Control",
"no-cache, no-store, must-revalidate, max-age=0, s-maxage=0",
],
["Pragma", "no-cache"],
["Expires", "0"],
]);
});
it("should set proper cache with clamped values", async () => {
{
let { req, res } = faker({ cache_seconds: 200_000 }, data_stats);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
[
"Cache-Control",
`max-age=${CACHE_TTL.STATS_CARD.MAX}, ` +
`s-maxage=${CACHE_TTL.STATS_CARD.MAX}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
],
]);
}
// note i'm using block scoped vars
{
let { req, res } = faker({ cache_seconds: 0 }, data_stats);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
[
"Cache-Control",
`max-age=${CACHE_TTL.STATS_CARD.MIN}, ` +
`s-maxage=${CACHE_TTL.STATS_CARD.MIN}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
],
]);
}
{
let { req, res } = faker({ cache_seconds: -10_000 }, data_stats);
await api(req, res);
expect(res.setHeader.mock.calls).toEqual([
["Content-Type", "image/svg+xml"],
[
"Cache-Control",
`max-age=${CACHE_TTL.STATS_CARD.MIN}, ` +
`s-maxage=${CACHE_TTL.STATS_CARD.MIN}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
],
]);
}
});
it("should allow changing ring_color", async () => {
const { req, res } = faker(
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
expect(apiMock).toHaveBeenCalledWith(
{
username: "anuraghazra",
hide: "issues,prs,contribs",
show_icons: true,
hide_border: true,
line_height: 100,
title_color: "fff",
ring_color: "0000ff",
icon_color: "fff",
text_color: "fff",
bg_color: "fff",
},
data_stats,
);
await api(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderStatsCard(stats, {
hide: ["issues", "prs", "contribs"],
show_icons: true,
hide_border: true,
line_height: 100,
title_color: "fff",
ring_color: "0000ff",
icon_color: "fff",
text_color: "fff",
bg_color: "fff",
}),
null,
);
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", errorCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("temporary-error-svg");
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
});
it("should render error card when wrong locale is provided", async () => {
const { req, res } = faker({ locale: "asdf" }, data_stats);
it("should not persist permanent stats errors returned by core", async () => {
apiMock.mockResolvedValue({
status: "error - permanent",
content: "permanent-error-svg",
});
await api(req, res);
const req = createRequest("username=anuraghazra");
const res = createResponse();
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "Something went wrong",
secondaryMessage: "Language not found",
}),
await router(req, res);
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
expect(apiMock).toHaveBeenCalledWith(
{
username: "anuraghazra",
},
null,
);
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("permanent-error-svg");
expect(storeRequestMock).not.toHaveBeenCalled();
});
it("should render error card when include_all_commits true and upstream API fails", async () => {
mock
.onGet(
"https://api.github.com/search/commits?per_page=1&q=author:anuraghazra",
)
.reply(200, { error: "Some test error message" });
it("should reject blacklisted usernames before calling core logic", async () => {
const req = createRequest("username=renovate-bot");
const res = createResponse();
const { req, res } = faker(
{ username: "anuraghazra", include_all_commits: true },
data_stats,
);
await router(req, res);
await api(req, res);
expect(apiMock).not.toHaveBeenCalled();
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith(
"render-error:This username is blacklisted",
);
expect(storeRequestMock).not.toHaveBeenCalled();
});
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "Could not fetch data from GitHub REST API.",
secondaryMessage: "Please try again later",
}),
);
// Received SVG output should not contain string "https://tiny.one/readme-stats"
expect(res.send.mock.calls[0][0]).not.toContain(
"https://tiny.one/readme-stats",
it("should reject non-whitelisted usernames before calling core logic", async () => {
getConfigMock.mockReturnValue({ whitelist: ["allowed-user"] });
const req = createRequest("username=blocked-user");
const res = createResponse();
await router(req, res);
expect(apiMock).not.toHaveBeenCalled();
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith(
"render-error:This username is not whitelisted",
);
expect(storeRequestMock).not.toHaveBeenCalled();
});
});
+25 -66
View File
@@ -1,81 +1,40 @@
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { bench, describe, vi } from "vitest";
import { beforeAll, bench, describe, vi } from "vitest";
import api from "../../api-renamed/index.js";
const stats = {
name: "Anurag Hazra",
totalStars: 100,
totalCommits: 200,
totalIssues: 300,
totalPRs: 400,
totalPRsMerged: 320,
mergedPRsPercentage: 80,
totalReviews: 50,
totalDiscussionsStarted: 10,
totalDiscussionsAnswered: 40,
contributedTo: 50,
rank: null,
};
const data_stats = {
data: {
user: {
name: stats.name,
repositoriesContributedTo: { totalCount: stats.contributedTo },
commits: {
totalCommitContributions: stats.totalCommits,
},
reviews: {
totalPullRequestReviewContributions: stats.totalReviews,
},
pullRequests: { totalCount: stats.totalPRs },
mergedPullRequests: { totalCount: stats.totalPRsMerged },
openIssues: { totalCount: stats.totalIssues },
closedIssues: { totalCount: 0 },
followers: { totalCount: 0 },
repositoryDiscussions: { totalCount: stats.totalDiscussionsStarted },
repositoryDiscussionComments: {
totalCount: stats.totalDiscussionsAnswered,
},
repositories: {
totalCount: 1,
nodes: [{ stargazers: { totalCount: 100 } }],
pageInfo: {
hasNextPage: false,
endCursor: "cursor",
},
},
},
},
};
import { data_stats } from "../utils.js";
const mock = new MockAdapter(axios);
const faker = (query, data) => {
const req = {
query: {
username: "anuraghazra",
...query,
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").replyOnce(200, data);
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
return { req, res };
};
let router;
describe("/api", () => {
beforeAll(async () => {
vi.stubEnv("CACHE_SECONDS", "");
vi.stubEnv("GIST_WHITELIST", "");
vi.stubEnv("POSTGRES_URL", "");
vi.stubEnv("WHITELIST", "");
({ default: router } = await import("../../router.js"));
mock.onPost("https://api.github.com/graphql").reply(200, data_stats);
});
describe("bench /api", () => {
bench(
"base",
async () => {
const { req, res } = faker({}, data_stats);
const req = {
headers: {},
url: "/api?username=anuraghazra",
};
const res = createResponse();
await api(req, res);
await router(req, res);
},
{ warmupIterations: 50 },
);
@@ -1,22 +0,0 @@
import { bench, describe } from "vitest";
import { calculateRank } from "../../src/calculateRank.js";
describe("calculateRank", () => {
bench(
"base",
async () => {
calculateRank({
all_commits: false,
commits: 1300,
prs: 1500,
issues: 4500,
reviews: 1000,
repos: 0,
stars: 600000,
followers: 50000,
});
},
{ warmupIterations: 50 },
);
});
+27 -39
View File
@@ -1,54 +1,42 @@
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { bench, describe, vi } from "vitest";
import { beforeAll, bench, describe, vi } from "vitest";
import gist from "../../api-renamed/gist.js";
const gist_data = {
data: {
viewer: {
gist: {
description:
"List of countries and territories in English and Spanish: name, continent, capital, dial code, country codes, TLD, and area in sq km. Lista de países y territorios en Inglés y Español: nombre, continente, capital, código de teléfono, códigos de país, dominio y área en km cuadrados. Updated 2023",
owner: {
login: "Yizack",
},
stargazerCount: 33,
forks: {
totalCount: 11,
},
files: [
{
name: "countries.json",
language: {
name: "JSON",
},
size: 85858,
},
],
},
},
},
};
import { happy_path_gist_data } from "../utils.js";
const mock = new MockAdapter(axios);
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
describe("test /api/gist", () => {
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
let router;
beforeAll(async () => {
vi.stubEnv("CACHE_SECONDS", "");
vi.stubEnv("GIST_WHITELIST", "");
vi.stubEnv("POSTGRES_URL", "");
vi.stubEnv("WHITELIST", "");
({ default: router } = await import("../../router.js"));
mock
.onPost("https://api.github.com/graphql")
.reply(200, happy_path_gist_data);
});
describe("bench /api/gist", () => {
bench(
"base",
async () => {
const req = {
query: {
id: "bbfce31e0217a3689c8d961a356cb10d",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
headers: {},
url: "/api/gist?id=happy-gist-id",
};
const res = createResponse();
await gist(req, res);
await router(req, res);
},
{ warmupIterations: 50 },
);
+25 -38
View File
@@ -1,53 +1,40 @@
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { bench, describe, vi } from "vitest";
import { beforeAll, bench, describe, vi } from "vitest";
import pin from "../../api-renamed/pin.js";
const data_repo = {
repository: {
username: "anuraghazra",
name: "convoychat",
stargazers: {
totalCount: 38000,
},
description: "Help us take over the world! React + TS + GraphQL Chat App",
primaryLanguage: {
color: "#2b7489",
id: "MDg6TGFuZ3VhZ2UyODc=",
name: "TypeScript",
},
forkCount: 100,
isTemplate: false,
},
};
const data_user = {
data: {
user: { repository: data_repo.repository },
organization: null,
},
};
import { data_user } from "../utils.js";
const mock = new MockAdapter(axios);
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
describe("/api/pin", () => {
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
let router;
beforeAll(async () => {
vi.stubEnv("CACHE_SECONDS", "");
vi.stubEnv("GIST_WHITELIST", "");
vi.stubEnv("POSTGRES_URL", "");
vi.stubEnv("WHITELIST", "");
({ default: router } = await import("../../router.js"));
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
});
describe("bench /api/pin", () => {
bench(
"base",
async () => {
const req = {
query: {
username: "anuraghazra",
repo: "convoychat",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
headers: {},
url: "/api/pin?username=anuraghazra&repo=convoychat",
};
const res = createResponse();
await pin(req, res);
await router(req, res);
},
{ warmupIterations: 50 },
);
-91
View File
@@ -1,91 +0,0 @@
import { describe, expect, it } from "vitest";
import { getCardColors } from "../src/common/color";
describe("Test color.js", () => {
it("getCardColors: should return expected values", () => {
let colors = getCardColors({
title_color: "f00",
text_color: "0f0",
ring_color: "0000ff",
icon_color: "00f",
bg_color: "fff",
border_color: "fff",
theme: "dark",
});
expect(colors).toStrictEqual({
titleColor: "#f00",
textColor: "#0f0",
iconColor: "#00f",
ringColor: "#0000ff",
bgColor: "#fff",
borderColor: "#fff",
});
});
it("getCardColors: should fallback to default colors if color is invalid", () => {
let colors = getCardColors({
title_color: "invalidcolor",
text_color: "0f0",
icon_color: "00f",
bg_color: "fff",
border_color: "invalidColor",
theme: "dark",
});
expect(colors).toStrictEqual({
titleColor: "#2f80ed",
textColor: "#0f0",
iconColor: "#00f",
ringColor: "#2f80ed",
bgColor: "#fff",
borderColor: "#e4e2e2",
});
});
it("getCardColors: should fallback to specified theme colors if is not defined", () => {
let colors = getCardColors({
theme: "dark",
});
expect(colors).toStrictEqual({
titleColor: "#fff",
textColor: "#9f9f9f",
ringColor: "#fff",
iconColor: "#79ff97",
bgColor: "#151515",
borderColor: "#e4e2e2",
});
});
it("getCardColors: should return ring color equal to title color if not ring color is defined", () => {
let colors = getCardColors({
title_color: "f00",
text_color: "0f0",
icon_color: "00f",
bg_color: "fff",
border_color: "fff",
theme: "dark",
});
expect(colors).toStrictEqual({
titleColor: "#f00",
textColor: "#0f0",
iconColor: "#00f",
ringColor: "#f00",
bgColor: "#fff",
borderColor: "#fff",
});
});
it("getCardColors: should fallback to default theme if theme is invalid", () => {
let colors = getCardColors({
theme: "invalidTheme",
});
expect(colors).toStrictEqual({
titleColor: "#2f80ed",
textColor: "#434d58",
iconColor: "#4c71f2",
ringColor: "#2f80ed",
bgColor: "#fffefe",
borderColor: "#e4e2e2",
});
});
});
+242 -122
View File
@@ -3,221 +3,341 @@
*/
import axios from "axios";
import { beforeAll, describe, expect, test } from "vitest";
import { renderGistCard } from "../../src/cards/gist.js";
import { renderRepoCard } from "../../src/cards/repo.js";
import { renderStatsCard } from "../../src/cards/stats.js";
import { renderTopLanguages } from "../../src/cards/top-languages.js";
import { renderWakatimeCard } from "../../src/cards/wakatime.js";
import MockAdapter from "axios-mock-adapter";
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
const REPO = "curly-fiesta";
const USER = "catelinemnemosyne";
const STATS_CARD_USER = "e2eninja";
const GIST_ID = "372cef55fd897b31909fdeb3a7262758";
const STATS_DATA = {
name: "CodeNinja",
totalPRs: 1,
totalReviews: 0,
totalCommits: 3,
totalIssues: 1,
totalStars: 1,
contributedTo: 0,
rank: {
level: "C",
percentile: 98.73972605284538,
const STATS_MOCK_RESPONSE = {
data: {
user: {
name: "CodeNinja",
login: STATS_CARD_USER,
repositoriesContributedTo: { totalCount: 0 },
commits: {
totalCommitContributions: 3,
},
reviews: {
totalPullRequestReviewContributions: 0,
},
pullRequests: { totalCount: 1 },
openIssues: { totalCount: 1 },
closedIssues: { totalCount: 0 },
followers: { totalCount: 0 },
repositories: {
totalCount: 1,
nodes: [{ name: REPO, stargazerCount: 1 }],
pageInfo: {
hasNextPage: false,
endCursor: "cursor",
},
},
},
},
};
const LANGS_DATA = {
HTML: {
color: "#e34c26",
name: "HTML",
size: 1721,
},
CSS: {
color: "#663399",
name: "CSS",
size: 930,
},
JavaScript: {
color: "#f1e05a",
name: "JavaScript",
size: 1912,
const COMMITS_SEARCH_MOCK_RESPONSE = {
total_count: 3,
};
const TOP_LANGS_MOCK_RESPONSE = {
data: {
user: {
repositories: {
nodes: [
{
name: REPO,
languages: {
edges: [
{
size: 1721,
node: {
color: "#e34c26",
name: "HTML",
},
},
{
size: 930,
node: {
color: "#663399",
name: "CSS",
},
},
{
size: 1912,
node: {
color: "#f1e05a",
name: "JavaScript",
},
},
],
},
},
],
},
},
},
};
const WAKATIME_DATA = {
human_readable_range: "last week",
is_already_updating: false,
is_coding_activity_visible: true,
is_including_today: false,
is_other_usage_visible: false,
is_stuck: false,
is_up_to_date: false,
is_up_to_date_pending_future: false,
percent_calculated: 0,
range: "all_time",
status: "pending_update",
timeout: 15,
username: USER,
writes_only: false,
const WAKATIME_MOCK_RESPONSE = {
data: {
human_readable_range: "last week",
is_already_updating: false,
is_coding_activity_visible: true,
is_including_today: false,
is_other_usage_visible: false,
is_stuck: false,
is_up_to_date: false,
is_up_to_date_pending_future: false,
percent_calculated: 0,
range: "all_time",
status: "pending_update",
timeout: 15,
username: USER,
writes_only: false,
},
};
const REPOSITORY_DATA = {
name: REPO,
nameWithOwner: `${USER}/cra-test`,
isPrivate: false,
isArchived: false,
isTemplate: false,
stargazers: {
totalCount: 1,
const REPO_MOCK_RESPONSE = {
data: {
user: {
repository: {
name: REPO,
nameWithOwner: `${USER}/cra-test`,
isPrivate: false,
isArchived: false,
isTemplate: false,
stargazerCount: 1,
description: "Simple cra test repo.",
primaryLanguage: {
color: "#f1e05a",
id: "MDg6TGFuZ3VhZ2UxNDA=",
name: "JavaScript",
},
forkCount: 0,
},
},
organization: null,
},
description: "Simple cra test repo.",
primaryLanguage: {
color: "#f1e05a",
id: "MDg6TGFuZ3VhZ2UxNDA=",
name: "JavaScript",
},
forkCount: 0,
starCount: 1,
};
/**
* @typedef {import("../../src/fetchers/types").GistData} GistData Gist data type.
*/
/**
* @type {GistData}
*/
const GIST_DATA = {
name: "link.txt",
nameWithOwner: "qwerty541/link.txt",
description:
"Trying to access this path on Windows 10 ver. 1803+ will breaks NTFS",
language: "Text",
starsCount: 1,
forksCount: 0,
const GIST_MOCK_RESPONSE = {
data: {
viewer: {
gist: {
description:
"Trying to access this path on Windows 10 ver. 1803+ will breaks NTFS",
owner: {
login: "qwerty541",
},
stargazerCount: 1,
forks: {
totalCount: 0,
},
files: [
{
name: "link.txt",
language: {
name: "Text",
},
size: 1,
},
],
},
},
},
};
const CACHE_BURST_STRING = `v=${new Date().getTime()}`;
describe("Fetch Cards", () => {
let VERCEL_PREVIEW_URL = "https://github-stats-extended.vercel.app";
const mock = new MockAdapter(axios, { onNoMatch: "passthrough" });
beforeAll(() => {
process.env.NODE_ENV = "development";
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
let router;
/**
* Renders a card locally through the backend router.
* @param {string} url Card URL to render through the router.
* @returns {Promise<string>} Rendered SVG markup.
*/
async function getLocalSvg(url) {
const req = {
headers: {},
url,
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
return res.end.mock.calls[0][0];
}
beforeAll(async () => {
vi.stubEnv("CACHE_SECONDS", "");
vi.stubEnv("GIST_WHITELIST", "");
vi.stubEnv("POSTGRES_URL", "");
vi.stubEnv("WHITELIST", "");
vi.stubEnv("PAT_1", "dummyPAT1");
vi.stubEnv("PAT_2", "dummyPAT2");
({ default: router } = await import("../../router.js"));
mock.onPost("https://api.github.com/graphql").reply((config) => {
const { query, variables } = JSON.parse(config.data);
if (
query.includes("query userInfo") &&
variables?.login === STATS_CARD_USER
) {
return [200, STATS_MOCK_RESPONSE];
}
if (query.includes("query topLanguages") && variables?.login === USER) {
return [200, TOP_LANGS_MOCK_RESPONSE];
}
if (query.includes("query getRepo")) {
return [200, REPO_MOCK_RESPONSE];
}
if (query.includes("query gistInfo")) {
return [200, GIST_MOCK_RESPONSE];
}
return [500, { error: "Unhandled GraphQL request in e2e test" }];
});
mock
.onGet(
`https://api.github.com/search/commits?per_page=1&q=author:${STATS_CARD_USER}`,
)
.reply(200, COMMITS_SEARCH_MOCK_RESPONSE);
mock
.onGet(
`https://wakatime.com/api/v1/users/${USER}/stats?is_including_today=true`,
)
.reply(200, WAKATIME_MOCK_RESPONSE);
});
afterAll(() => {
mock.restore();
vi.unstubAllEnvs();
});
describe("Fetch Cards", () => {
const VERCEL_PREVIEW_URL = "https://github-stats-extended-preview.vercel.app";
test("retrieve stats card", async () => {
expect(VERCEL_PREVIEW_URL).toBeDefined();
const cardPath = `/api?username=${STATS_CARD_USER}&include_all_commits=true&${CACHE_BURST_STRING}`;
// Check if the Vercel preview instance stats card function is up and running.
await expect(
axios.get(`${VERCEL_PREVIEW_URL}/api?username=${STATS_CARD_USER}`),
axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`),
).resolves.not.toThrow();
// Get local stats card.
const localStatsCardSVG = renderStatsCard(STATS_DATA, {
include_all_commits: true,
});
const localStatsCardSVG = await getLocalSvg(cardPath);
// Get the Vercel preview stats card response.
const serverStatsSvg = await axios.get(
`${VERCEL_PREVIEW_URL}/api?username=${STATS_CARD_USER}&include_all_commits=true&${CACHE_BURST_STRING}`,
);
const serverStatsSvg = await axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`);
// Check if stats card from deployment matches the stats card from local.
expect(serverStatsSvg.data).toEqual(localStatsCardSVG);
}, 15000);
}, 20000);
test("retrieve language card", async () => {
expect(VERCEL_PREVIEW_URL).toBeDefined();
const cardPath = `/api/top-langs?username=${USER}&${CACHE_BURST_STRING}`;
// Check if the Vercel preview instance language card function is up and running.
console.log(
`${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`,
);
await expect(
axios.get(
`${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`,
),
axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`),
).resolves.not.toThrow();
// Get local language card.
const localLanguageCardSVG = renderTopLanguages(LANGS_DATA);
const localLanguageCardSVG = await getLocalSvg(cardPath);
// Get the Vercel preview language card response.
const severLanguageSVG = await axios.get(
`${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`,
const serverLanguageSVG = await axios.get(
`${VERCEL_PREVIEW_URL}${cardPath}`,
);
// Check if language card from deployment matches the local language card.
expect(severLanguageSVG.data).toEqual(localLanguageCardSVG);
}, 15000);
expect(serverLanguageSVG.data).toEqual(localLanguageCardSVG);
}, 20000);
test("retrieve WakaTime card", async () => {
expect(VERCEL_PREVIEW_URL).toBeDefined();
const cardPath = `/api/wakatime?username=${USER}&${CACHE_BURST_STRING}`;
// Check if the Vercel preview instance WakaTime function is up and running.
await expect(
axios.get(`${VERCEL_PREVIEW_URL}/api/wakatime?username=${USER}`),
axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`),
).resolves.not.toThrow();
// Get local WakaTime card.
const localWakaCardSVG = renderWakatimeCard(WAKATIME_DATA);
const localWakaCardSVG = await getLocalSvg(cardPath);
// Get the Vercel preview WakaTime card response.
const serverWakaTimeSvg = await axios.get(
`${VERCEL_PREVIEW_URL}/api/wakatime?username=${USER}&${CACHE_BURST_STRING}`,
`${VERCEL_PREVIEW_URL}${cardPath}`,
);
// Check if WakaTime card from deployment matches the local WakaTime card.
expect(serverWakaTimeSvg.data).toEqual(localWakaCardSVG);
}, 15000);
}, 20000);
test("retrieve repo card", async () => {
expect(VERCEL_PREVIEW_URL).toBeDefined();
const cardPath = `/api/pin?username=${USER}&repo=${REPO}&${CACHE_BURST_STRING}`;
// Check if the Vercel preview instance Repo function is up and running.
await expect(
axios.get(
`${VERCEL_PREVIEW_URL}/api/pin/?username=${USER}&repo=${REPO}&${CACHE_BURST_STRING}`,
),
axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`),
).resolves.not.toThrow();
// Get local repo card.
const localRepoCardSVG = renderRepoCard(REPOSITORY_DATA);
const localRepoCardSVG = await getLocalSvg(cardPath);
// Get the Vercel preview repo card response.
const serverRepoSvg = await axios.get(
`${VERCEL_PREVIEW_URL}/api/pin/?username=${USER}&repo=${REPO}&${CACHE_BURST_STRING}`,
);
const serverRepoSvg = await axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`);
// Check if Repo card from deployment matches the local Repo card.
expect(serverRepoSvg.data).toEqual(localRepoCardSVG);
}, 15000);
}, 20000);
test("retrieve gist card", async () => {
expect(VERCEL_PREVIEW_URL).toBeDefined();
const cardPath = `/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`;
// Check if the Vercel preview instance Gist function is up and running.
await expect(
axios.get(
`${VERCEL_PREVIEW_URL}/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`,
),
axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`),
).resolves.not.toThrow();
// Get local gist card.
const localGistCardSVG = renderGistCard(GIST_DATA);
const localGistCardSVG = await getLocalSvg(cardPath);
// Get the Vercel preview gist card response.
const serverGistSvg = await axios.get(
`${VERCEL_PREVIEW_URL}/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`,
);
const serverGistSvg = await axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`);
// Check if Gist card from deployment matches the local Gist card.
expect(serverGistSvg.data).toEqual(localGistCardSVG);
}, 15000);
}, 20000);
});
+124 -136
View File
@@ -1,161 +1,149 @@
// @ts-check
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import { getConfig, gist } from "@stats-organization/github-readme-stats-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import gist from "../api-renamed/gist.js";
import { renderGistCard } from "../src/cards/gist.js";
import router from "../router.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
import { renderError } from "../src/common/render.js";
import { getUserAccessByName, storeRequest } from "../src/common/database.js";
import { gist_data } from "./test-data/gist-data.js";
import "@testing-library/jest-dom/vitest";
const gist_not_found_data = {
data: {
viewer: {
gist: null,
},
},
};
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
vi.mock(import("@stats-organization/github-readme-stats-core"), async () => {
const { mockCore } = await import("./utils.js");
return mockCore();
});
describe("Test /api/gist", () => {
it("should test the request", async () => {
const req = {
query: {
id: "bbfce31e0217a3689c8d961a356cb10d",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
vi.mock(import("../src/common/database.js"), async (importOriginal) => ({
...(await importOriginal()),
storeRequest: vi.fn(),
getUserAccessByName: vi.fn(),
}));
await gist(req, res);
const gistMock = vi.mocked(gist);
const getConfigMock = vi.mocked(getConfig);
const storeRequestMock = vi.mocked(storeRequest);
const getUserAccessByNameMock = vi.mocked(getUserAccessByName);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderGistCard({
name: gist_data.data.viewer.gist.files[0].name,
nameWithOwner: `${gist_data.data.viewer.gist.owner.login}/${gist_data.data.viewer.gist.files[0].name}`,
description: gist_data.data.viewer.gist.description,
language: gist_data.data.viewer.gist.files[0].language.name,
starsCount: gist_data.data.viewer.gist.stargazerCount,
forksCount: gist_data.data.viewer.gist.forks.totalCount,
}),
);
const createRequest = (search) => ({
headers: {},
url: `/api/gist?${search}`,
});
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
const defaultCacheHeader =
`max-age=${CACHE_TTL.GIST_CARD.DEFAULT}, ` +
`s-maxage=${CACHE_TTL.GIST_CARD.DEFAULT}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;
const errorCacheHeader =
`max-age=${CACHE_TTL.ERROR}, ` +
`s-maxage=${CACHE_TTL.ERROR}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;
beforeEach(() => {
gistMock.mockReset();
getConfigMock.mockReset().mockReturnValue({});
storeRequestMock.mockReset().mockResolvedValue(undefined);
getUserAccessByNameMock.mockReset().mockResolvedValue(null);
// CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally
delete process.env.CACHE_SECONDS;
});
describe("Test /api/gist backend routing", () => {
it("happy path should pass query params, respond with gist content and persist request", async () => {
gistMock.mockResolvedValue({
status: "success",
content: "mock-gist-svg",
});
const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d&theme=dark");
const res = createResponse();
await router(req, res);
expect(gistMock).toHaveBeenCalledWith({
id: "bbfce31e0217a3689c8d961a356cb10d",
theme: "dark",
});
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(req.query).toEqual({
id: "bbfce31e0217a3689c8d961a356cb10d",
theme: "dark",
});
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-gist-svg");
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
});
it("should get the query options", async () => {
const req = {
query: {
id: "bbfce31e0217a3689c8d961a356cb10d",
title_color: "fff",
icon_color: "fff",
text_color: "fff",
bg_color: "fff",
show_owner: true,
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
it("should use the shorter error cache for temporary gist errors", async () => {
gistMock.mockResolvedValue({
status: "error - temporary",
content: "temporary-error-svg",
});
await gist(req, res);
const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d");
const res = createResponse();
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderGistCard(
{
name: gist_data.data.viewer.gist.files[0].name,
nameWithOwner: `${gist_data.data.viewer.gist.owner.login}/${gist_data.data.viewer.gist.files[0].name}`,
description: gist_data.data.viewer.gist.description,
language: gist_data.data.viewer.gist.files[0].language.name,
starsCount: gist_data.data.viewer.gist.stargazerCount,
forksCount: gist_data.data.viewer.gist.forks.totalCount,
},
{ ...req.query },
),
);
await router(req, res);
expect(gistMock).toHaveBeenCalledWith({
id: "bbfce31e0217a3689c8d961a356cb10d",
});
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", errorCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("temporary-error-svg");
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
});
it("should render error if gist is not found", async () => {
const req = {
query: {
id: "bbfce31e0217a3689c8d961a356cb10d",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock
.onPost("https://api.github.com/graphql")
.reply(200, gist_not_found_data);
it("should not persist permanent gist errors returned by core", async () => {
gistMock.mockResolvedValue({
status: "error - permanent",
content: "permanent-error-svg",
});
await gist(req, res);
const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d");
const res = createResponse();
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({ message: "Gist not found" }),
);
await router(req, res);
expect(gistMock).toHaveBeenCalledWith({
id: "bbfce31e0217a3689c8d961a356cb10d",
});
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("permanent-error-svg");
expect(storeRequestMock).not.toHaveBeenCalled();
});
it("should render error if wrong locale is provided", async () => {
const req = {
query: {
id: "bbfce31e0217a3689c8d961a356cb10d",
locale: "asdf",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
it("should reject non-whitelisted gist ids before calling core logic", async () => {
getConfigMock.mockReturnValue({ gistWhitelist: ["allowed-gist-id"] });
await gist(req, res);
const req = createRequest("id=blocked-gist-id");
const res = createResponse();
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "Something went wrong",
secondaryMessage: "Language not found",
}),
);
});
await router(req, res);
it("should have proper cache", async () => {
const req = {
query: {
id: "bbfce31e0217a3689c8d961a356cb10d",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
await gist(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.setHeader).toHaveBeenCalledWith(
"Cache-Control",
`max-age=${CACHE_TTL.GIST_CARD.DEFAULT}, ` +
`s-maxage=${CACHE_TTL.GIST_CARD.DEFAULT}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
expect(gistMock).not.toHaveBeenCalled();
expect(getUserAccessByNameMock).not.toHaveBeenCalled();
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith(
"render-error:This gist ID is not whitelisted",
);
expect(storeRequestMock).not.toHaveBeenCalled();
});
});
-11
View File
@@ -1,11 +0,0 @@
import { describe, expect, it } from "vitest";
import { encodeHTML } from "../src/common/html.js";
describe("Test html.js", () => {
it("should test encodeHTML", () => {
expect(encodeHTML(`<html>hello world<,.#4^&^@%!))`)).toBe(
"&#60;html&#62;hello world&#60;,.#4^&#38;^@%!))",
);
});
});
+33 -13
View File
@@ -4,9 +4,15 @@
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import patInfo, { RATE_LIMIT_SECONDS } from "../api-renamed/status/pat-info.js";
import {
afterAll,
afterEach,
beforeAll,
describe,
expect,
it,
vi,
} from "vitest";
const mock = new MockAdapter(axios);
@@ -57,20 +63,35 @@ const bad_credentials_error = {
message: "Bad credentials",
};
let RATE_LIMIT_SECONDS, patInfo;
beforeAll(async () => {
vi.stubEnv("PAT_1", "testPAT1");
vi.stubEnv("PAT_2", "testPAT2");
vi.stubEnv("PAT_3", "testPAT3");
vi.stubEnv("PAT_4", "testPAT4");
const { logger } =
await import("@stats-organization/github-readme-stats-core");
vi.spyOn(logger, "log").mockImplementation(() => {});
vi.spyOn(logger, "error").mockImplementation(() => {});
({ RATE_LIMIT_SECONDS, default: patInfo } =
await import("../api-renamed/status/pat-info.js"));
});
afterEach(() => {
mock.reset();
vi.unstubAllEnvs();
// modules may cache environment variables, so we need to reset them
vi.resetModules();
});
afterAll(() => {
vi.restoreAllMocks();
});
describe("Test /api/status/pat-info", () => {
beforeAll(() => {
// reset patenv first so that they are not populated with local envs
process.env = {};
process.env.PAT_1 = "testPAT1";
process.env.PAT_2 = "testPAT2";
process.env.PAT_3 = "testPAT3";
process.env.PAT_4 = "testPAT4";
});
it("should return only 'validPATs' if all PATs are valid", async () => {
mock
.onPost("https://api.github.com/graphql")
@@ -243,7 +264,6 @@ describe("Test /api/status/pat-info", () => {
});
it("should have proper cache when error is thrown", async () => {
mock.reset();
mock.onPost("https://api.github.com/graphql").networkError();
const { req, res } = faker({}, {});
+71 -162
View File
@@ -1,175 +1,84 @@
// @ts-check
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import { pin } from "@stats-organization/github-readme-stats-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import pin from "../api-renamed/pin.js";
import { renderRepoCard } from "../src/cards/repo.js";
import router from "../router.js";
import { CACHE_TTL, DURATIONS } from "../src/common/cache.js";
import { renderError } from "../src/common/render.js";
import { getUserAccessByName, storeRequest } from "../src/common/database.js";
import { data_repo, data_user } from "./test-data/pin-data.js";
import "@testing-library/jest-dom/vitest";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
vi.mock(import("@stats-organization/github-readme-stats-core"), async () => {
const { mockCore } = await import("./utils.js");
return mockCore();
});
describe("Test /api/pin", () => {
it("should test the request", async () => {
const req = {
query: {
vi.mock(import("../src/common/database.js"), async (importOriginal) => ({
...(await importOriginal()),
storeRequest: vi.fn(),
getUserAccessByName: vi.fn(),
}));
const pinMock = vi.mocked(pin);
const storeRequestMock = vi.mocked(storeRequest);
const getUserAccessByNameMock = vi.mocked(getUserAccessByName);
const createRequest = (search = "") => ({
headers: {},
url: `/api/pin?${search}`,
});
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
const defaultCacheHeader =
`max-age=${CACHE_TTL.PIN_CARD.DEFAULT}, ` +
`s-maxage=${CACHE_TTL.PIN_CARD.DEFAULT}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`;
beforeEach(() => {
pinMock.mockReset();
storeRequestMock.mockReset().mockResolvedValue(undefined);
getUserAccessByNameMock.mockReset().mockResolvedValue(null);
// CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally
delete process.env.CACHE_SECONDS;
});
describe("Test /api/pin backend routing", () => {
it("happy path should pass query params and user PAT, respond with pin content and persist request", async () => {
getUserAccessByNameMock.mockResolvedValue({ token: "user-pat" });
pinMock.mockResolvedValue({
status: "success",
content: "mock-pin-svg",
});
const req = createRequest(
"username=anuraghazra&repo=convoychat&theme=dark",
);
const res = createResponse();
await router(req, res);
expect(getUserAccessByNameMock).toHaveBeenCalledWith("anuraghazra");
expect(pinMock).toHaveBeenCalledWith(
{
username: "anuraghazra",
repo: "convoychat",
theme: "dark",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
// @ts-ignore
renderRepoCard({
...data_repo.repository,
starCount: data_repo.repository.stargazers.totalCount,
}),
);
});
it("should get the query options", async () => {
const req = {
query: {
username: "anuraghazra",
repo: "convoychat",
title_color: "fff",
icon_color: "fff",
text_color: "fff",
bg_color: "fff",
full_name: "1",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderRepoCard(
// @ts-ignore
{
...data_repo.repository,
starCount: data_repo.repository.stargazers.totalCount,
},
{ ...req.query },
),
);
});
it("should render error card if user repo not found", async () => {
const req = {
query: {
username: "anuraghazra",
repo: "convoychat",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock
.onPost("https://api.github.com/graphql")
.reply(200, { data: { user: { repository: null }, organization: null } });
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({ message: "User Repository Not found" }),
);
});
it("should render error card if org repo not found", async () => {
const req = {
query: {
username: "anuraghazra",
repo: "convoychat",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock
.onPost("https://api.github.com/graphql")
.reply(200, { data: { user: null, organization: { repository: null } } });
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({ message: "Organization Repository Not found" }),
);
});
it("should render error card if wrong locale provided", async () => {
const req = {
query: {
username: "anuraghazra",
repo: "convoychat",
locale: "asdf",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "Something went wrong",
secondaryMessage: "Language not found",
}),
);
});
it("should have proper cache", async () => {
const req = {
query: {
username: "anuraghazra",
repo: "convoychat",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.setHeader).toHaveBeenCalledWith(
"Cache-Control",
`max-age=${CACHE_TTL.PIN_CARD.DEFAULT}, ` +
`s-maxage=${CACHE_TTL.PIN_CARD.DEFAULT}, ` +
`stale-while-revalidate=${DURATIONS.ONE_DAY}`,
"user-pat",
);
expect(req.query).toEqual({
username: "anuraghazra",
repo: "convoychat",
theme: "dark",
});
expect(res.setHeader.mock.calls).toEqual([
["Cache-Control", defaultCacheHeader],
["Content-Type", "image/svg+xml"],
]);
expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-pin-svg");
expect(storeRequestMock).toHaveBeenCalledExactlyOnceWith(req);
});
});
@@ -1,41 +0,0 @@
// @ts-check
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import api from "../../api-renamed/index.js";
import { renderError } from "../../src/common/render.js";
import { data_stats } from "../test-data/api-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/", () => {
it("should render error card if username not in whitelist", async () => {
const req = {
query: {
username: "renovate-bot",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").replyOnce(200, data_stats);
await api(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is not whitelisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
});
@@ -1,41 +0,0 @@
// @ts-check
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import gist from "../../api-renamed/gist.js";
import { renderError } from "../../src/common/render.js";
import { gist_data } from "../test-data/gist-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/gist with gist whitelist", () => {
it("should render error card if id not in whitelist", async () => {
const req = {
query: {
id: "9bae0392ee3a26bac5cc388a6c8b1469",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
await gist(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This gist ID is not whitelisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
});
@@ -1,64 +0,0 @@
// @ts-check
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import pin from "../../api-renamed/pin.js";
import { renderError } from "../../src/common/render.js";
import { data_user } from "../test-data/pin-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/pin", () => {
it("should render error card if username not in whitelist", async () => {
const req = {
query: {
username: "renovate-bot",
repo: "convoychat",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is not whitelisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
it("should render error card if missing required parameters", async () => {
const req = {
query: {},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
await pin(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is not whitelisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
});
@@ -1,41 +0,0 @@
// @ts-check
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import topLangs from "../../api-renamed/top-langs.js";
import { renderError } from "../../src/common/render.js";
import { data_langs } from "../test-data/langs-data.js";
const mock = new MockAdapter(axios);
afterEach(() => {
mock.reset();
});
describe("Test /api/top-langs", () => {
it("should render error card if username not in whitelist", async () => {
const req = {
query: {
username: "renovate-bot",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
await topLangs(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is not whitelisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
});
});
@@ -0,0 +1,770 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test /api contract > should match the gradient-background response snapshot 1`] = `
{
"content": "<svg width="450" height="195" viewBox="0 0 450 195" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId">Anurag Hazra's GitHub Stats, Rank: A+</title>
<desc id="descId">Total Stars Earned: 14099, Total Commits (last year) : 200, Total PRs: 4000, Total Issues: 340, Contributed to (last year): 51</desc>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #ffffff;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #ffffff;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.rank-text {
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: #ffffff;
animation: scaleInAnimation 0.3s ease-in-out forwards;
}
.rank-percentile-header {
font-size: 14px;
}
.rank-percentile-text {
font-size: 16px;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: #ffffff;
display: none;
}
.rank-circle-rim {
stroke: #ffffff;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #ffffff;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
@keyframes rankAnimation {
from {
stroke-dashoffset: 251.32741228718345;
}
to {
stroke-dashoffset: 25.665014275610705;
}
}
@media (prefers-color-scheme: dark) {
.header { fill: #2f80ed; }
.card-bg { fill: url(#gradient-dark); stroke: #e4e2e2; }
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #434d58;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.rank-text {
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58;
animation: scaleInAnimation 0.3s ease-in-out forwards;
}
.rank-percentile-header {
font-size: 14px;
}
.rank-percentile-text {
font-size: 16px;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: #4c71f2;
display: none;
}
.rank-circle-rim {
stroke: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
@keyframes rankAnimation {
from {
stroke-dashoffset: 251.32741228718345;
}
to {
stroke-dashoffset: 25.665014275610705;
}
}
}
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
<defs>
<linearGradient id="gradient" gradientTransform="rotate(35)" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#4158d0"/>,<stop offset="50%" stop-color="#c850c0"/>,<stop offset="100%" stop-color="#ffcc70"/>
</linearGradient>
<linearGradient id="gradient-dark" gradientTransform="rotate(45)" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#60A5FA"/>,<stop offset="100%" stop-color="#4ADE80"/>
</linearGradient>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="449" fill="url(#gradient)" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<text x="0" y="0" class="header" data-testid="header">Anurag Hazra's GitHub Stats</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(365, 47.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40"/>
<circle class="rank-circle" cx="-10" cy="8" r="40"/>
<g class="rank-text">
<text x="-5" y="3" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="level-rank-icon">
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g class="stagger" style="animation-delay: 450ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="stars">14.1k</text>
</g>
</g><g transform="translate(0, 25)">
<g class="stagger" style="animation-delay: 600ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total Commits (last year):</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="commits">200</text>
</g>
</g><g transform="translate(0, 50)">
<g class="stagger" style="animation-delay: 750ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="prs">4k</text>
</g>
</g><g transform="translate(0, 75)">
<g class="stagger" style="animation-delay: 900ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="issues">340</text>
</g>
</g><g transform="translate(0, 100)">
<g class="stagger" style="animation-delay: 1050ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="contribs">51</text>
</g>
</g>
</svg>
</g>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api contract > should match the private missing-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">This username is not whitelisted</tspan>
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api contract > should match the public blacklisted-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">This username is blacklisted</tspan>
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api contract > should match the public happy-path response snapshot 1`] = `
{
"content": "<svg width="450" height="195" viewBox="0 0 450 195" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId">Anurag Hazra's GitHub Stats, Rank: A+</title>
<desc id="descId">Total Stars Earned: 14099, Total Commits (last year) : 200, Total PRs: 4000, Total Issues: 340, Contributed to (last year): 51</desc>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #2f80ed;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #434d58;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.rank-text {
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58;
animation: scaleInAnimation 0.3s ease-in-out forwards;
}
.rank-percentile-header {
font-size: 14px;
}
.rank-percentile-text {
font-size: 16px;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: #4c71f2;
display: none;
}
.rank-circle-rim {
stroke: #2f80ed;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #2f80ed;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
@keyframes rankAnimation {
from {
stroke-dashoffset: 251.32741228718345;
}
to {
stroke-dashoffset: 25.665014275610705;
}
}
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="449" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<text x="0" y="0" class="header" data-testid="header">Anurag Hazra's GitHub Stats</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(365, 47.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40"/>
<circle class="rank-circle" cx="-10" cy="8" r="40"/>
<g class="rank-text">
<text x="-5" y="3" alignment-baseline="central" dominant-baseline="central" text-anchor="middle" data-testid="level-rank-icon">
A+
</text>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g class="stagger" style="animation-delay: 450ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total Stars Earned:</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="stars">14.1k</text>
</g>
</g><g transform="translate(0, 25)">
<g class="stagger" style="animation-delay: 600ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total Commits (last year):</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="commits">200</text>
</g>
</g><g transform="translate(0, 50)">
<g class="stagger" style="animation-delay: 750ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total PRs:</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="prs">4k</text>
</g>
</g><g transform="translate(0, 75)">
<g class="stagger" style="animation-delay: 900ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Total Issues:</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="issues">340</text>
</g>
</g><g transform="translate(0, 100)">
<g class="stagger" style="animation-delay: 1050ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5">Contributed to (last year):</text>
<text class="stat bold" x="204.01" y="12.5" data-testid="contribs">51</text>
</g>
</g>
</svg>
</g>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api contract > should match the public many-params response snapshot 1`] = `
{
"content": "<svg width="540" height="365" viewBox="0 0 540 365" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId">a custom title, Rank: A+</title>
<desc id="descId">कुल अर्जित सितारे: 4100, कुल commits (2024) : 200, कुल PR: 4000, कुल PR का विलय: 3200, मर्ज किए गए PRs प्रतिशत: 80.0, कुल PRs की समीक्षा की गई: 1234, कुल चर्चाएँ शुरू हुईं: 222, कुल चर्चाओं के उत्तर: 111, (पिछले वर्ष) में योगदान दिया: 51</desc>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #123456;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #abcdef;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.rank-text {
font: 800 24px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef;
animation: scaleInAnimation 0.3s ease-in-out forwards;
}
.rank-percentile-header {
font-size: 14px;
}
.rank-percentile-text {
font-size: 16px;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: #ff00aa;
display: block;
}
.rank-circle-rim {
stroke: #654321;
fill: none;
stroke-width: 6;
opacity: 0.2;
}
.rank-circle {
stroke: #654321;
stroke-dasharray: 250;
fill: none;
stroke-width: 6;
stroke-linecap: round;
opacity: 0.8;
transform-origin: -10px 8px;
transform: rotate(-90deg);
animation: rankAnimation 1s forwards ease-in-out;
}
@keyframes rankAnimation {
from {
stroke-dashoffset: 251.32741228718345;
}
to {
stroke-dashoffset: 26.378312814443284;
}
}
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
* { animation-duration: 0s !important; animation-delay: 0s !important; }
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="539" fill="#0f172a" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<text x="0" y="0" class="header" data-testid="header">a custom title</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="rank-circle" transform="translate(463.5, 132.5)">
<circle class="rank-circle-rim" cx="-10" cy="8" r="40"/>
<circle class="rank-circle" cx="-10" cy="8" r="40"/>
<g class="rank-text">
<svg x="-38" y="-30" height="66" width="66" aria-hidden="true" viewBox="0 0 16 16" version="1.1" data-view-component="true" data-testid="github-rank-icon">
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"/>
</svg>
</g>
</g>
<svg x="0" y="0">
<g transform="translate(0, 0)">
<g class="stagger" style="animation-delay: 450ms" transform="translate(25, 0)">
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
</svg>
<text class="stat not_bold" x="25" y="12.5">कुल अर्जित सितारे:</text>
<text class="stat not_bold" x="219.01" y="12.5" data-testid="stars">4100</text>
</g>
</g><g transform="translate(0, 32)">
<g class="stagger" style="animation-delay: 600ms" transform="translate(25, 0)">
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"/>
</svg>
<text class="stat not_bold" x="25" y="12.5">कुल commits (2024):</text>
<text class="stat not_bold" x="219.01" y="12.5" data-testid="commits">200</text>
</g>
</g><g transform="translate(0, 64)">
<g class="stagger" style="animation-delay: 750ms" transform="translate(25, 0)">
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>
</svg>
<text class="stat not_bold" x="25" y="12.5">कुल PR:</text>
<text class="stat not_bold" x="219.01" y="12.5" data-testid="prs">4000</text>
</g>
</g><g transform="translate(0, 96)">
<g class="stagger" style="animation-delay: 900ms" transform="translate(25, 0)">
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z"/>
</svg>
<text class="stat not_bold" x="25" y="12.5">कुल PR का विलय:</text>
<text class="stat not_bold" x="219.01" y="12.5" data-testid="prs_merged">3200</text>
</g>
</g><g transform="translate(0, 128)">
<g class="stagger" style="animation-delay: 1050ms" transform="translate(25, 0)">
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M13.442 2.558a.625.625 0 0 1 0 .884l-10 10a.625.625 0 1 1-.884-.884l10-10a.625.625 0 0 1 .884 0zM4.5 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zm7 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</svg>
<text class="stat not_bold" x="25" y="12.5">मर्ज किए गए PRs प्रतिशत:</text>
<text class="stat not_bold" x="219.01" y="12.5" data-testid="prs_merged_percentage">80.0 %</text>
</g>
</g><g transform="translate(0, 160)">
<g class="stagger" style="animation-delay: 1200ms" transform="translate(25, 0)">
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.677 1.367-1.931 2.637-3.022C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"/>
</svg>
<text class="stat not_bold" x="25" y="12.5">कुल PRs की समीक्षा की गई:</text>
<text class="stat not_bold" x="219.01" y="12.5" data-testid="reviews">1234</text>
</g>
</g><g transform="translate(0, 192)">
<g class="stagger" style="animation-delay: 1350ms" transform="translate(25, 0)">
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"/>
</svg>
<text class="stat not_bold" x="25" y="12.5">कुल चर्चाएँ शुरू हुईं:</text>
<text class="stat not_bold" x="219.01" y="12.5" data-testid="discussions_started">222</text>
</g>
</g><g transform="translate(0, 224)">
<g class="stagger" style="animation-delay: 1500ms" transform="translate(25, 0)">
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/>
</svg>
<text class="stat not_bold" x="25" y="12.5">कुल चर्चाओं के उत्तर:</text>
<text class="stat not_bold" x="219.01" y="12.5" data-testid="discussions_answered">111</text>
</g>
</g><g transform="translate(0, 256)">
<g class="stagger" style="animation-delay: 1650ms" transform="translate(25, 0)">
<svg data-testid="icon" class="icon" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>
</svg>
<text class="stat not_bold" x="25" y="12.5">(पिछले वर्ष) में योगदान दिया:</text>
<text class="stat not_bold" x="219.01" y="12.5" data-testid="contribs">51</text>
</g>
</g>
</svg>
</g>
</svg>",
"graphqlRequest": "{"query":"\\nquery userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation], $includeUserRepositories: Boolean!) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection(from: $startTime) {\\n totalCommitContributions\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(\\n first: 1\\n contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]\\n includeUserRepositories: $includeUserRepositories\\n ) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n contributionsCollection {\\n contributionYears\\n }\\n ...RepoStars\\n }\\n}\\nfragment RepoStars on User {\\n repositories(\\n first: 100\\n after: $after\\n ownerAffiliations: $ownerAffiliations\\n orderBy: {direction: DESC, field: STARGAZERS}\\n ) {\\n totalCount\\n nodes {\\n ...RepoNode\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\\nfragment RepoNode on Repository {\\n name\\n stargazerCount\\n}","variables":{"login":"anuraghazra","after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"],"includeUserRepositories":false}}",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api contract > should match the public missing-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
<tspan x="25" dy="18" class="gray"/>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api contract > should render error card in same theme as requested card 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#0a0f0b" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abd200 }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #68b587 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#0a0f0b" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
<tspan x="25" dy="18" class="gray"/>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
@@ -0,0 +1,282 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test /api/gist contract > should match the private missing-id response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">This gist ID is not whitelisted</tspan>
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/gist contract > should match the public happy-path response snapshot 1`] = `
{
"content": "<svg width="400" height="180" viewBox="0 0 400 180" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #2f80ed;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #434d58;
}
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.icon { fill: #586069 }
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="399" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<svg class="icon" x="0" y="-13" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"/>
</svg>
</g><g transform="translate(25, 0)">
<text x="0" y="0" class="header" data-testid="header">countries.json</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<text class="description" x="25" y="-5">
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish: name,</tspan><tspan dy="1.2em" x="25">continent, capital, dial code, country codes, TLD, and area in</tspan><tspan dy="1.2em" x="25">sq km. Lista de países y territorios en Inglés y Español:</tspan><tspan dy="1.2em" x="25">nombre, continente, capital, código de teléfono, códigos de</tspan><tspan dy="1.2em" x="25">país, dominio y área en km cuadrados. Updated 2023</tspan>
</text>
<g transform="translate(30, 105)">
<g transform="translate(0, 0)">
<g data-testid="primary-lang">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#292929"/>
<text data-testid="lang-name" class="gray" x="15">JSON</text>
</g>
</g><g transform="translate(53.67999954223633, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(107.59999923706056, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="forksCount" class="gray">11</text></g></g>
</g>
</g>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/gist contract > should match the public many-params response snapshot 1`] = `
{
"content": "<svg width="400" height="180" viewBox="0 0 400 180" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #123456;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #ffabcd;
}
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #ffabcd }
.icon { fill: #ff00aa }
@media (prefers-color-scheme: dark) {
.header { fill: #123456; }
.card-bg { fill: #0f172a; stroke: #fedcba; }
.description {
fill: #abcdef;
}
.gray { fill: #abcdef }
.icon { fill: #aa00ff }
}
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="399" fill="#0f172a" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<svg class="icon" x="0" y="-13" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"/>
</svg>
</g><g transform="translate(25, 0)">
<text x="0" y="0" class="header" data-testid="header">Yizack/countries.json</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<text class="description" x="25" y="-5">
<tspan dy="1.2em" x="25">List of countries and territories in English and Spanish: name,</tspan><tspan dy="1.2em" x="25">continent, capital, dial code, country codes, TLD, and area in</tspan><tspan dy="1.2em" x="25">sq km. Lista de países y territorios en Inglés y Español:</tspan><tspan dy="1.2em" x="25">nombre, continente, capital, código de teléfono, códigos de</tspan><tspan dy="1.2em" x="25">país, dominio y área en km cuadrados. Updated 2023</tspan>
</text>
<g transform="translate(30, 105)">
<g transform="translate(0, 0)">
<g data-testid="primary-lang">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#292929"/>
<text data-testid="lang-name" class="gray" x="15">JSON</text>
</g>
</g><g transform="translate(53.67999954223633, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="starsCount" class="gray">33</text></g></g><g transform="translate(107.59999923706056, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="forksCount" class="gray">11</text></g></g>
</g>
</g>
</svg>",
"graphqlRequest": "{"query":"\\nquery gistInfo($gistName: String!) {\\n viewer {\\n gist(name: $gistName) {\\n description\\n owner {\\n login\\n }\\n stargazerCount\\n forks {\\n totalCount\\n }\\n files {\\n ...GistFileInfo\\n }\\n }\\n }\\n}\\nfragment GistFileInfo on GistFile {\\n name\\n language {\\n name\\n }\\n size\\n}","variables":{"gistName":"happy-gist-id"}}",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/gist contract > should match the public missing-id response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">Missing params "id" make sure you pass the parameters in URL</tspan>
<tspan x="25" dy="18" class="gray">/api/gist?id=GIST_ID</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
@@ -0,0 +1,575 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test /api/pin contract > should match the private missing-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">This username is not whitelisted</tspan>
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/pin contract > should match the private non-whitelisted username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">This username is not whitelisted</tspan>
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/pin contract > should match the public archived-repo response snapshot 1`] = `
{
"content": "<svg width="400" height="150" viewBox="0 0 400 150" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #2f80ed;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #434d58;
}
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2; stroke: #434d58 }
.badge text { fill: #434d58 }
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: #586069;
display: block;
}
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
* { animation-duration: 0s !important; animation-delay: 0s !important; }
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="399" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<svg class="icon" x="0" y="-13" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>
</svg>
</g><g transform="translate(25, 0)">
<text x="0" y="0" class="header" data-testid="header">convoychat</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<g data-testid="badge" class="badge" transform="translate(320, -18)">
<rect stroke-width="1" width="70" height="20" x="-12" y="-14" ry="10" rx="10"/>
<text x="23" y="-5" alignment-baseline="central" dominant-baseline="central" text-anchor="middle">
Archived
</text>
</g>
<text class="description" x="25" y="-5">
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable</tspan><tspan dy="1.2em" x="25">React, TypeScript and GraphQL chat app that has enough</tspan><tspan dy="1.2em" x="25">text to wrap across multiple lines in the repository card.</tspan>
</text>
<g transform="translate(30, 75)">
<g transform="translate(0, 0)">
<g data-testid="primary-lang">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#2b7489"/>
<text data-testid="lang-name" class="gray" x="15">TypeScript</text>
</g>
</g><g transform="translate(80.53999967575072, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(140.4199994087219, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="forkcount" class="gray">100</text></g></g>
</g>
<svg x="0" y="0"><g transform="translate(-3, 98)">
</g></svg>
</g>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/pin contract > should match the public blacklisted-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">This username is blacklisted</tspan>
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/pin contract > should match the public happy-path response snapshot 1`] = `
{
"content": "<svg width="400" height="150" viewBox="0 0 400 150" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #2f80ed;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #434d58;
}
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2; stroke: #434d58 }
.badge text { fill: #434d58 }
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: #586069;
display: block;
}
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
* { animation-duration: 0s !important; animation-delay: 0s !important; }
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="399" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<svg class="icon" x="0" y="-13" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>
</svg>
</g><g transform="translate(25, 0)">
<text x="0" y="0" class="header" data-testid="header">convoychat</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<text class="description" x="25" y="-5">
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable</tspan><tspan dy="1.2em" x="25">React, TypeScript and GraphQL chat app that has enough</tspan><tspan dy="1.2em" x="25">text to wrap across multiple lines in the repository card.</tspan>
</text>
<g transform="translate(30, 75)">
<g transform="translate(0, 0)">
<g data-testid="primary-lang">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#2b7489"/>
<text data-testid="lang-name" class="gray" x="15">TypeScript</text>
</g>
</g><g transform="translate(80.53999967575072, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(140.4199994087219, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="forkcount" class="gray">100</text></g></g>
</g>
<svg x="0" y="0"><g transform="translate(-3, 98)">
</g></svg>
</g>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/pin contract > should match the public many-params response snapshot 1`] = `
{
"content": "<svg width="560" height="233" viewBox="0 0 560 233" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #123456;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.description {
font: 400 13px 'Segoe UI', Ubuntu, Sans-Serif;fill: #a9fef7;
}
.gray { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #a9fef7 }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2; stroke: #a9fef7 }
.badge text { fill: #a9fef7 }
.stat { font: 400 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #a9fef7 }
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
fill: #f8d847;
display: block;
}
@media (prefers-color-scheme: dark) {
.header { fill: #123456; }
.card-bg { fill: #193549; stroke: #fedcba; }
.description {
fill: #75eeb2;
}
.gray { fill: #75eeb2 }
.badge rect { stroke: #75eeb2 }
.badge text { fill: #75eeb2 }
.stat { fill: #75eeb2 }
.icon { fill: #0480ef; }
}
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
* { animation-duration: 0s !important; animation-delay: 0s !important; }
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="559" fill="#141321" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<svg class="icon" x="0" y="-13" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>
</svg>
</g><g transform="translate(25, 0)">
<text x="0" y="0" class="header" data-testid="header">anuraghazra/convoychat</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<text class="description" x="25" y="-5">
<tspan dy="1.2em" x="25">Help us take over the world with a deeply customizable React, TypeScript and GraphQL...</tspan>
</text>
<g transform="translate(30, 45)">
<g transform="translate(0, 0)">
<g data-testid="primary-lang">
<circle data-testid="lang-color" cx="0" cy="-5" r="6" fill="#2b7489"/>
<text data-testid="lang-name" class="gray" x="15">TypeScript</text>
</g>
</g><g transform="translate(80.53999967575072, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="stargazers" class="gray">38k</text></g></g><g transform="translate(140.4199994087219, 0)"><g transform="translate(0, 0)">
<svg class="icon" y="-12" viewBox="0 0 16 16" version="1.1" width="16" height="16">
<path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
</svg>
</g><g transform="translate(20, 0)"><text data-testid="forkcount" class="gray">100</text></g></g>
</g>
<svg x="0" y="0"><g transform="translate(-3, 68)">
<g transform="translate(0, 0)"><g transform="translate(0, 0)">
<g class="stagger" style="animation-delay: 450ms" transform="translate(25, 0)"><a href="https://github.com/search?q=repo%3Aanuraghazra%2Fconvoychat%20author%3Aanuraghazra&amp;type=pullrequests">
<text class="stat bold" y="12.5">my created PRs:</text>
<text class="stat bold" x="139.01" y="12.5" data-testid="prs_authored">1234</text></a>
</g>
</g><g transform="translate(210, 0)">
<g class="stagger" style="animation-delay: 600ms" transform="translate(25, 0)"><a href="https://github.com/search?q=repo%3Aanuraghazra%2Fconvoychat%20commenter%3Aanuraghazra+-author%3Aanuraghazra&amp;type=pullrequests">
<text class="stat bold" y="12.5">my commented PRs:</text>
<text class="stat bold" x="139.01" y="12.5" data-testid="prs_commented">2345</text></a>
</g>
</g></g><g transform="translate(0, 30)"><g transform="translate(0, 0)">
<g class="stagger" style="animation-delay: 750ms" transform="translate(25, 0)"><a href="https://github.com/search?q=repo%3Aanuraghazra%2Fconvoychat%20reviewed-by%3Aanuraghazra+-author%3Aanuraghazra&amp;type=pullrequests">
<text class="stat bold" y="12.5">my reviewed PRs:</text>
<text class="stat bold" x="139.01" y="12.5" data-testid="prs_reviewed">3456</text></a>
</g>
</g><g transform="translate(210, 0)">
<g class="stagger" style="animation-delay: 900ms" transform="translate(25, 0)"><a href="https://github.com/search?q=repo%3Aanuraghazra%2Fconvoychat%20author%3Aanuraghazra&amp;type=issues">
<text class="stat bold" y="12.5">my created issues:</text>
<text class="stat bold" x="139.01" y="12.5" data-testid="issues_authored">4567</text></a>
</g>
</g></g><g transform="translate(0, 60)"><g transform="translate(0, 0)">
<g class="stagger" style="animation-delay: 1050ms" transform="translate(25, 0)"><a href="https://github.com/search?q=repo%3Aanuraghazra%2Fconvoychat%20commenter%3Aanuraghazra+-author%3Aanuraghazra&amp;type=issues">
<text class="stat bold" y="12.5">my commented issues:</text>
<text class="stat bold" x="139.01" y="12.5" data-testid="issues_commented">5678</text></a>
</g>
</g></g>
</g></svg>
</g>
</svg>",
"graphqlRequest": "{"query":"\\nquery getRepo($login: String!, $repo: String!) {\\n user(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n organization(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n}\\nfragment RepoInfo on Repository {\\n name\\n nameWithOwner\\n isPrivate\\n isArchived\\n isTemplate\\n stargazerCount\\n description\\n primaryLanguage {\\n color\\n id\\n name\\n }\\n forkCount\\n}","variables":{"login":"anuraghazra","repo":"convoychat"}}",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/pin contract > should match the public missing-params response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">Missing params "username", "repo" make sure you pass the parameters in URL</tspan>
<tspan x="25" dy="18" class="gray">/api/pin?username=USERNAME&amp;repo=REPO_NAME</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/pin contract > should render error card in same theme as requested card 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#0a0f0b" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abd200 }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #68b587 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#0a0f0b" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">Missing params "username", "repo" make sure you pass the parameters in URL</tspan>
<tspan x="25" dy="18" class="gray">/api/pin?username=USERNAME&amp;repo=REPO_NAME</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
@@ -0,0 +1,483 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test /api/top-langs contract > should match the private missing-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">This username is not whitelisted</tspan>
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/top-langs contract > should match the private non-whitelisted username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">This username is not whitelisted</tspan>
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/top-langs contract > should match the public blacklisted-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">This username is blacklisted</tspan>
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/top-langs contract > should match the public happy-path response snapshot 1`] = `
{
"content": "<svg width="300" height="285" viewBox="0 0 300 285" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #2f80ed;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
@keyframes slideInAnimation {
from {
width: 0;
}
to {
width: calc(100%-100px);
}
}
@keyframes growWidthAnimation {
from {
width: 0;
}
to {
width: 100%;
}
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #434d58;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.bold { font-weight: 700 }
.lang-name {
font: 400 11px "Segoe UI", Ubuntu, Sans-Serif;
fill: #434d58;
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
#rect-mask rect{
animation: slideInAnimation 1s ease-in-out forwards;
}
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
}
.progress-background { fill: #ddd; }
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="299" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<text x="0" y="0" class="header" data-testid="header">Most Used Languages</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<svg data-testid="lang-items" x="25">
<g transform="translate(0, 0)">
<g class="stagger" style="animation-delay: 450ms">
<text data-testid="lang-name" x="2" y="15" class="lang-name">Rust</text>
<text x="215" y="34" class="lang-name">70.92%</text>
<svg width="205" x="0" y="25">
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8"/>
<svg data-testid="lang-progress" width="70.92198581560284%">
<rect height="8" fill="#dea584" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 750ms;"/>
</svg>
</svg>
</g>
</g><g transform="translate(0, 40)">
<g class="stagger" style="animation-delay: 600ms">
<text data-testid="lang-name" x="2" y="15" class="lang-name">HTML</text>
<text x="215" y="34" class="lang-name">12.77%</text>
<svg width="205" x="0" y="25">
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8"/>
<svg data-testid="lang-progress" width="12.76595744680851%">
<rect height="8" fill="#0f0" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 900ms;"/>
</svg>
</svg>
</g>
</g><g transform="translate(0, 80)">
<g class="stagger" style="animation-delay: 750ms">
<text data-testid="lang-name" x="2" y="15" class="lang-name">JavaScript</text>
<text x="215" y="34" class="lang-name">8.51%</text>
<svg width="205" x="0" y="25">
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8"/>
<svg data-testid="lang-progress" width="8.51063829787234%">
<rect height="8" fill="#0ff" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1050ms;"/>
</svg>
</svg>
</g>
</g><g transform="translate(0, 120)">
<g class="stagger" style="animation-delay: 900ms">
<text data-testid="lang-name" x="2" y="15" class="lang-name">TypeScript</text>
<text x="215" y="34" class="lang-name">6.38%</text>
<svg width="205" x="0" y="25">
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8"/>
<svg data-testid="lang-progress" width="6.382978723404255%">
<rect height="8" fill="#3178c6" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1200ms;"/>
</svg>
</svg>
</g>
</g><g transform="translate(0, 160)">
<g class="stagger" style="animation-delay: 1050ms">
<text data-testid="lang-name" x="2" y="15" class="lang-name">Java</text>
<text x="215" y="34" class="lang-name">1.42%</text>
<svg width="205" x="0" y="25">
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="205" height="8"/>
<svg data-testid="lang-progress" width="2%">
<rect height="8" fill="#f00" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1350ms;"/>
</svg>
</svg>
</g>
</g>
</svg>
</g>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/top-langs contract > should match the public many-params response snapshot 1`] = `
{
"content": "<svg width="420" height="110" viewBox="0 0 420 110" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #123456;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
@keyframes slideInAnimation {
from {
width: 0;
}
to {
width: calc(100%-100px);
}
}
@keyframes growWidthAnimation {
from {
width: 0;
}
to {
width: 100%;
}
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #abcdef;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.bold { font-weight: 700 }
.lang-name {
font: 400 11px "Segoe UI", Ubuntu, Sans-Serif;
fill: #abcdef;
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
#rect-mask rect{
animation: slideInAnimation 1s ease-in-out forwards;
}
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
}
.progress-background { fill: #ddd; }
@media (prefers-color-scheme: dark) {
.header { fill: #123456; }
.card-bg { fill: #0f172a; stroke: #e4e2e2; }
.stat { fill: #abcdef; }
.lang-name { fill: #abcdef; }
.progress-background { fill: #ddd; }
}
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
* { animation-duration: 0s !important; animation-delay: 0s !important; }
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="419" fill="#0f172a" stroke-opacity="0"/>
<g data-testid="main-card-body" transform="translate(0, 25)">
<svg data-testid="lang-items" x="25">
<mask id="rect-mask">
<rect x="0" y="0" width="370" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="0" y="0" width="256.98" height="8" fill="#3178c6"/>
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="256.98" y="0" width="60.57" height="8" fill="#f00"/>
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="317.55" y="0" width="52.45" height="8" fill="#00f"/>
<g transform="translate(0, 25)">
<g transform="translate(0, 0)"><g transform="translate(0, 0)">
<g class="stagger" style="animation-delay: 450ms">
<circle cx="5" cy="6" r="5" fill="#3178c6"/>
<text data-testid="lang-name" x="15" y="10" class="lang-name">
TypeScript 19.0 B
</text>
</g>
</g><g transform="translate(0, 25)">
<g class="stagger" style="animation-delay: 600ms">
<circle cx="5" cy="6" r="5" fill="#f00"/>
<text data-testid="lang-name" x="15" y="10" class="lang-name">
Java 4.5 B
</text>
</g>
</g></g><g transform="translate(150, 0)"><g transform="translate(0, 0)">
<g class="stagger" style="animation-delay: 450ms">
<circle cx="5" cy="6" r="5" fill="#00f"/>
<text data-testid="lang-name" x="15" y="10" class="lang-name">
Python 3.9 B
</text>
</g>
</g></g>
</g>
</svg>
</g>
</svg>",
"graphqlRequest": "{"query":"\\nquery topLanguages($login: String!, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n repositories(ownerAffiliations: $ownerAffiliations, isFork: false, first: 100) {\\n nodes {\\n ...TopLanguagesRepository\\n }\\n }\\n }\\n}\\nfragment TopLanguagesRepository on Repository {\\n name\\n languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {\\n edges {\\n ...TopLanguage\\n }\\n }\\n}\\nfragment TopLanguage on LanguageEdge {\\n size\\n node {\\n color\\n name\\n }\\n}","variables":{"login":"anuraghazra","ownerAffiliations":["OWNER","COLLABORATOR"]}}",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/top-langs contract > should match the public missing-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
<tspan x="25" dy="18" class="gray"/>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/top-langs contract > should render error card in same theme as requested card 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#0a0f0b" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abd200 }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #68b587 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#0a0f0b" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
<tspan x="25" dy="18" class="gray"/>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
@@ -0,0 +1,594 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test /api/wakatime contract > should match the private missing-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">This username is not whitelisted</tspan>
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/wakatime contract > should match the private non-whitelisted username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">This username is not whitelisted</tspan>
<tspan x="25" dy="18" class="gray">Please deploy your own instance</tspan>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/wakatime contract > should match the public happy-path response snapshot 1`] = `
{
"content": "<svg width="495" height="195" viewBox="0 0 495 195" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #2f80ed;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #434d58;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
@keyframes slideInAnimation {
from {
width: 0;
}
to {
width: calc(100%-100px);
}
}
@keyframes growWidthAnimation {
from {
width: 0;
}
to {
width: 100%;
}
}
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
#rect-mask rect{
animation: slideInAnimation 1s ease-in-out forwards;
}
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
fill: #2f80ed;
}
.progress-background { fill: #434d58; }
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="494" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<text x="0" y="0" class="header" data-testid="header">WakaTime Stats (last 7 days)</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<svg x="0" y="0" width="100%">
<g transform="translate(0, 0)">
<g class="stagger" style="animation-delay: 450ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5" data-testid="TypeScript">TypeScript:</text>
<text class="stat" x="350" y="12.5">12 hrs</text>
<svg width="220" x="110" y="4">
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8"/>
<svg data-testid="lang-progress" width="52%">
<rect height="8" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 750ms;"/>
</svg>
</svg>
</g>
</g><g transform="translate(0, 25)">
<g class="stagger" style="animation-delay: 600ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5" data-testid="JavaScript">JavaScript:</text>
<text class="stat" x="350" y="12.5">6 hrs 30 mins</text>
<svg width="220" x="110" y="4">
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8"/>
<svg data-testid="lang-progress" width="28%">
<rect height="8" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 900ms;"/>
</svg>
</svg>
</g>
</g><g transform="translate(0, 50)">
<g class="stagger" style="animation-delay: 750ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5" data-testid="Other">Other:</text>
<text class="stat" x="350" y="12.5">3 hrs</text>
<svg width="220" x="110" y="4">
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8"/>
<svg data-testid="lang-progress" width="13%">
<rect height="8" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1050ms;"/>
</svg>
</svg>
</g>
</g><g transform="translate(0, 75)">
<g class="stagger" style="animation-delay: 900ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5" data-testid="YAML">YAML:</text>
<text class="stat" x="350" y="12.5">1 hr</text>
<svg width="220" x="110" y="4">
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8"/>
<svg data-testid="lang-progress" width="4%">
<rect height="8" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1200ms;"/>
</svg>
</svg>
</g>
</g><g transform="translate(0, 100)">
<g class="stagger" style="animation-delay: 1050ms" transform="translate(25, 0)">
<text class="stat bold" y="12.5" data-testid="JSON">JSON:</text>
<text class="stat" x="350" y="12.5">30 mins</text>
<svg width="220" x="110" y="4">
<rect data-testid="progress-background" class="progress-background" rx="5" ry="5" x="0" y="0" width="220" height="8"/>
<svg data-testid="lang-progress" width="3%">
<rect height="8" rx="5" ry="5" x="0" y="0" class="lang-progress" style="animation-delay: 1350ms;"/>
</svg>
</svg>
</g>
</g>
</svg>
</g>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/wakatime contract > should match the public inaccessible-profile response snapshot 1`] = `
{
"content": "<svg width="495" height="150" viewBox="0 0 495 150" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #2f80ed;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #434d58;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
@keyframes slideInAnimation {
from {
width: 0;
}
to {
width: calc(100%-100px);
}
}
@keyframes growWidthAnimation {
from {
width: 0;
}
to {
width: 100%;
}
}
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
#rect-mask rect{
animation: slideInAnimation 1s ease-in-out forwards;
}
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
fill: #2f80ed;
}
.progress-background { fill: #434d58; }
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="4.5" height="99%" stroke="#e4e2e2" width="494" fill="#fffefe" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<text x="0" y="0" class="header" data-testid="header">WakaTime Stats</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<svg x="0" y="0" width="100%">
<g transform="translate(0, 0)">
<text x="25" y="11" class="stat bold">WakaTime user profile not public</text>
</g>
</svg>
</g>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/wakatime contract > should match the public many-params response snapshot 1`] = `
{
"content": "<svg width="620" height="140" viewBox="0 0 620 140" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="descId">
<title id="titleId"/>
<desc id="descId"/>
<style>
.header {
font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif;
fill: #ff3456;
animation: fadeInAnimation 0.8s ease-in-out forwards;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.header { font-size: 15.5px; }
}
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #abcdef;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
@keyframes slideInAnimation {
from {
width: 0;
}
to {
width: calc(100%-100px);
}
}
@keyframes growWidthAnimation {
from {
width: 0;
}
to {
width: 100%;
}
}
.lang-name { font: 400 11px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abcdef }
#rect-mask rect{
animation: slideInAnimation 1s ease-in-out forwards;
}
.lang-progress{
animation: growWidthAnimation 0.6s ease-in-out forwards;
fill: #ff3456;
}
.progress-background { fill: #abcdef; }
@media (prefers-color-scheme: dark) {
.header { fill: #ff6543; }
.card-bg { fill: #193549; stroke: #fedcba; }
.stat {
font: 600 14px 'Segoe UI', Ubuntu, "Helvetica Neue", Sans-Serif; fill: #abcdef;
}
@supports(-moz-appearance: auto) {
/* Selector detects Firefox */
.stat { font-size:12px; }
}
.stagger {
opacity: 0;
animation: fadeInAnimation 0.3s ease-in-out forwards;
}
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.lang-name { fill: #abcdef }
.lang-progress { fill: #ff6543; }
.progress-background { fill: #abcdef; }
}
/* Animations */
@keyframes scaleInAnimation {
from {
transform: translate(-5px, 5px) scale(0);
}
to {
transform: translate(-5px, 5px) scale(1);
}
}
@keyframes fadeInAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
* { animation-duration: 0s !important; animation-delay: 0s !important; }
</style>
<defs>
</defs>
<rect data-testid="card-bg" class="card-bg" x="0.5" y="0.5" rx="12" height="99%" stroke="#fedcba" width="619" fill="#141321" stroke-opacity="1"/>
<g data-testid="card-title" transform="translate(25, 35)">
<g transform="translate(0, 0)">
<text x="0" y="0" class="header" data-testid="header">a custom title</text>
</g>
</g>
<g data-testid="main-card-body" transform="translate(0, 55)">
<svg x="0" y="0" width="100%">
<mask id="rect-mask">
<rect x="25" y="0" width="565" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="0" y="0" width="365.09200000000004" height="8" fill="#3178c6"/>
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="365.09200000000004" y="0" width="196.588" height="8" fill="#f1e05a"/>
<rect mask="url(#rect-mask)" data-testid="lang-progress" x="561.6800000000001" y="0" width="28.084" height="8" fill="#cb171e"/>
<g transform="translate(25, 25)">
<circle cx="5" cy="6" r="5" fill="#3178c6"/>
<text data-testid="lang-name" x="15" y="10" class="lang-name">
TypeScript - 61.88 %
</text>
</g>
<g transform="translate(292.5, 25)">
<circle cx="5" cy="6" r="5" fill="#f1e05a"/>
<text data-testid="lang-name" x="15" y="10" class="lang-name">
JavaScript - 33.32 %
</text>
</g>
<g transform="translate(25, 50)">
<circle cx="5" cy="6" r="5" fill="#cb171e"/>
<text data-testid="lang-name" x="15" y="10" class="lang-name">
YAML - 4.76 %
</text>
</g>
</svg>
</g>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=36000, s-maxage=36000, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/wakatime contract > should match the public missing-username response snapshot 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#fffefe" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #2f80ed }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #434d58 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#fffefe" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
<tspan x="25" dy="18" class="gray"/>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
exports[`Test /api/wakatime contract > should render error card in same theme as requested card 1`] = `
{
"content": "<svg width="576.5" height="120" viewBox="0 0 576.5 120" fill="#0a0f0b" xmlns="http://www.w3.org/2000/svg">
<style>
.text { font: 600 16px 'Segoe UI', Ubuntu, Sans-Serif; fill: #abd200 }
.small { font: 600 12px 'Segoe UI', Ubuntu, Sans-Serif; fill: #68b587 }
.gray { fill: #858585 }
</style>
<rect x="0.5" y="0.5" width="575.5" height="99%" rx="4.5" fill="#0a0f0b" stroke="#e4e2e2"/>
<text x="25" y="45" class="text">Something went wrong!</text>
<text data-testid="message" x="25" y="55" class="text small">
<tspan x="25" dy="18">Missing params "username" make sure you pass the parameters in URL</tspan>
<tspan x="25" dy="18" class="gray"/>
</text>
</svg>",
"headers": [
[
"Cache-Control",
"max-age=600, s-maxage=600, stale-while-revalidate=86400",
],
[
"Content-Type",
"image/svg+xml",
],
],
}
`;
+202 -22
View File
@@ -1,41 +1,221 @@
// @ts-check
import { logger } from "@stats-organization/github-readme-stats-core";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest";
import api from "../../api-renamed/index.js";
import { renderError } from "../../src/common/render.js";
import { data_stats } from "../test-data/api-data.js";
import { data_stats, normalizeSvg } from "../utils.js";
const mock = new MockAdapter(axios);
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
beforeEach(() => {
vi.stubEnv("CACHE_SECONDS", "");
vi.stubEnv("GIST_WHITELIST", "");
vi.stubEnv("POSTGRES_URL", "");
vi.stubEnv("WHITELIST", "");
mock.onPost("https://api.github.com/graphql").reply(200, data_stats);
});
beforeAll(() => {
vi.spyOn(logger, "log").mockImplementation(() => {});
vi.spyOn(logger, "error").mockImplementation(() => {});
});
afterEach(() => {
mock.reset();
vi.unstubAllEnvs();
// modules may cache environment variables, so we need to reset them
vi.resetModules();
});
describe("Test /api/", () => {
it("should render error card if username in blacklist", async () => {
afterAll(() => {
vi.restoreAllMocks();
});
describe("Test /api contract", () => {
it("should match the public happy-path response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
query: {
username: "renovate-bot",
},
headers: {},
url: "/api?username=anuraghazra",
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public many-params response snapshot", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data_stats);
const { default: router } = await import("../../router.js");
const params = new URLSearchParams({
username: "anuraghazra",
show_icons: "true",
card_width: "540",
line_height: "32",
title_color: "123456",
ring_color: "654321",
icon_color: "ff00aa",
text_color: "abcdef",
text_bold: "false",
bg_color: "0f172a",
exclude_repo: "repo-exclude-me",
custom_title: "a custom title",
locale: "hi",
disable_animations: "true",
border_radius: "12",
number_format: "long",
number_precision: "1",
border_color: "fedcba",
rank_icon: "github",
commits_year: "2024",
hide: "issues",
role: "OWNER,COLLABORATOR",
show: "reviews,prs_merged,prs_merged_percentage,discussions_started,discussions_answered",
});
const req = {
headers: {},
url: `/api?${params.toString()}`,
};
mock.onPost("https://api.github.com/graphql").replyOnce(200, data_stats);
const res = createResponse();
await api(req, res);
await router(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is blacklisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
expect(res.end).toHaveBeenCalledOnce();
expect({
graphqlRequest: mock.history.post[0].data,
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the gradient-background response snapshot", async () => {
const { default: router } = await import("../../router.js");
const params = new URLSearchParams({
username: "anuraghazra",
theme_light: "ambient_gradient",
bg_color_dark: "45,60A5FA,4ADE80",
});
const req = {
headers: {},
url: `/api?${params.toString()}`,
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public missing-username response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should render error card in same theme as requested card", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api?theme=merko",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public blacklisted-username response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api?username=renovate-bot",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the private missing-username response snapshot", async () => {
vi.stubEnv("WHITELIST", "anuraghazra");
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
});
+126 -20
View File
@@ -1,39 +1,145 @@
// @ts-check
import { logger } from "@stats-organization/github-readme-stats-core";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest";
import gist from "../../api-renamed/gist.js";
import { renderError } from "../../src/common/render.js";
import { gist_data } from "../test-data/gist-data.js";
import { happy_path_gist_data, normalizeSvg } from "../utils.js";
const mock = new MockAdapter(axios);
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
beforeEach(() => {
vi.stubEnv("CACHE_SECONDS", "");
vi.stubEnv("GIST_WHITELIST", "");
vi.stubEnv("POSTGRES_URL", "");
vi.stubEnv("WHITELIST", "");
mock
.onPost("https://api.github.com/graphql")
.reply(200, happy_path_gist_data);
});
beforeAll(() => {
vi.spyOn(logger, "log").mockImplementation(() => {});
vi.spyOn(logger, "error").mockImplementation(() => {});
});
afterEach(() => {
mock.reset();
vi.unstubAllEnvs();
// modules may cache environment variables, so we need to reset them
vi.resetModules();
});
describe("Test /api/gist", () => {
it("should render error if id is not provided", async () => {
afterAll(() => {
vi.restoreAllMocks();
});
describe("Test /api/gist contract", () => {
it("should match the public happy-path response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
query: {},
headers: {},
url: "/api/gist?id=happy-gist-id",
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public many-params response snapshot", async () => {
const { default: router } = await import("../../router.js");
const params = new URLSearchParams({
id: "happy-gist-id",
title_color: "123456",
icon_color_light: "ff00aa",
icon_color_dark: "aa00ff",
text_color_light: "ffabcd",
text_color_dark: "abcdef",
bg_color: "0f172a",
border_radius: "12",
border_color: "fedcba",
show_owner: "true",
});
const req = {
headers: {},
url: `/api/gist?${params.toString()}`,
};
mock.onPost("https://api.github.com/graphql").reply(200, gist_data);
const res = createResponse();
await gist(req, res);
await router(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: 'Missing params "id" make sure you pass the parameters in URL',
secondaryMessage: "/api/gist?id=GIST_ID",
renderOptions: { show_repo_link: false },
}),
);
expect(res.end).toHaveBeenCalledOnce();
expect({
graphqlRequest: mock.history.post[0].data,
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public missing-id response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/gist",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the private missing-id response snapshot", async () => {
vi.stubEnv("GIST_WHITELIST", "allowed-gist-id");
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/gist",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
});
+235 -40
View File
@@ -1,65 +1,260 @@
// @ts-check
import { logger } from "@stats-organization/github-readme-stats-core";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest";
import pin from "../../api-renamed/pin.js";
import { renderError } from "../../src/common/render.js";
import { data_user } from "../test-data/pin-data.js";
import { data_user, normalizeSvg } from "../utils.js";
const mock = new MockAdapter(axios);
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
beforeEach(() => {
vi.stubEnv("CACHE_SECONDS", "");
vi.stubEnv("GIST_WHITELIST", "");
vi.stubEnv("POSTGRES_URL", "");
vi.stubEnv("WHITELIST", "");
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
});
beforeAll(() => {
vi.spyOn(logger, "log").mockImplementation(() => {});
vi.spyOn(logger, "error").mockImplementation(() => {});
});
afterEach(() => {
mock.reset();
vi.unstubAllEnvs();
// modules may cache environment variables, so we need to reset them
vi.resetModules();
});
describe("Test /api/pin", () => {
it("should render error card if username in blacklist", async () => {
afterAll(() => {
vi.restoreAllMocks();
});
describe("Test /api/pin contract", () => {
it("should match the public happy-path response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
query: {
username: "renovate-bot",
repo: "convoychat",
},
headers: {},
url: "/api/pin?username=anuraghazra&repo=convoychat",
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
};
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
const res = createResponse();
await pin(req, res);
await router(req, res);
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is blacklisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should render error card if missing required parameters", async () => {
it("should match the public archived-repo response snapshot", async () => {
let archived_data_user = structuredClone(data_user);
archived_data_user.data.user.repository.isArchived = true;
mock
.onPost("https://api.github.com/graphql")
.reply(200, archived_data_user);
const { default: router } = await import("../../router.js");
const req = {
query: {},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
headers: {},
url: "/api/pin?username=anuraghazra&repo=convoychat",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public many-params response snapshot", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data_user);
mock
.onGet(
"https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+author:anuraghazra+type:pr",
)
.reply(200, { total_count: 1234 });
mock
.onGet(
"https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+commenter:anuraghazra+-author:anuraghazra+type:pr",
)
.reply(200, { total_count: 2345 });
mock
.onGet(
"https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+reviewed-by:anuraghazra+-author:anuraghazra+type:pr",
)
.reply(200, { total_count: 3456 });
mock
.onGet(
"https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+author:anuraghazra+type:issue",
)
.reply(200, { total_count: 4567 });
mock
.onGet(
"https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+commenter:anuraghazra+-author:anuraghazra+type:issue",
)
.reply(200, { total_count: 5678 });
await pin(req, res);
const { default: router } = await import("../../router.js");
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message:
'Missing params "username", "repo" make sure you pass the parameters in URL',
secondaryMessage: "/api/pin?username=USERNAME&amp;repo=REPO_NAME",
renderOptions: { show_repo_link: false },
}),
);
const params = new URLSearchParams({
username: "anuraghazra",
repo: "convoychat",
title_color: "123456",
theme_light: "radical",
theme_dark: "cobalt",
card_width: "560",
show_owner: "true",
show: "prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented",
show_icons: "false",
number_format: "long",
text_bold: "true",
line_height: "30",
border_radius: "12",
border_color: "fedcba",
description_lines_count: "1",
});
const req = {
headers: {},
url: `/api/pin?${params.toString()}`,
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
graphqlRequest: mock.history.post[0].data,
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public missing-params response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/pin",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should render error card in same theme as requested card", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/pin?theme=merko",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public blacklisted-username response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/pin?username=renovate-bot&repo=convoychat",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the private non-whitelisted username response snapshot", async () => {
vi.stubEnv("WHITELIST", "anuraghazra");
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/pin?username=martin-mfg",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the private missing-username response snapshot", async () => {
vi.stubEnv("WHITELIST", "anuraghazra");
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/pin",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
});
@@ -1,41 +1,211 @@
// @ts-check
import { logger } from "@stats-organization/github-readme-stats-core";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest";
import topLangs from "../../api-renamed/top-langs.js";
import { renderError } from "../../src/common/render.js";
import { data_langs } from "../test-data/langs-data.js";
import { data_langs, normalizeSvg } from "../utils.js";
const mock = new MockAdapter(axios);
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
beforeEach(() => {
vi.stubEnv("CACHE_SECONDS", "");
vi.stubEnv("GIST_WHITELIST", "");
vi.stubEnv("POSTGRES_URL", "");
vi.stubEnv("WHITELIST", "");
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
});
beforeAll(() => {
vi.spyOn(logger, "log").mockImplementation(() => {});
vi.spyOn(logger, "error").mockImplementation(() => {});
});
afterEach(() => {
mock.reset();
vi.unstubAllEnvs();
// modules may cache environment variables, so we need to reset them
vi.resetModules();
});
describe("Test /api/top-langs", () => {
it("should render error card if username in blacklist", async () => {
afterAll(() => {
vi.restoreAllMocks();
});
describe("Test /api/top-langs contract", () => {
it("should match the public happy-path response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
query: {
username: "renovate-bot",
},
};
const res = {
setHeader: vi.fn(),
send: vi.fn(),
headers: {},
url: "/api/top-langs?username=anuraghazra",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public many-params response snapshot", async () => {
mock.onPost("https://api.github.com/graphql").reply(200, data_langs);
await topLangs(req, res);
const { default: router } = await import("../../router.js");
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml");
expect(res.send).toHaveBeenCalledWith(
renderError({
message: "This username is blacklisted",
secondaryMessage: "Please deploy your own instance",
renderOptions: { show_repo_link: false },
}),
);
const params = new URLSearchParams({
username: "anuraghazra",
hide: "javascript,HTML",
hide_title: "true",
hide_border: "true",
card_width: "420",
title_color: "123456",
text_color: "abcdef",
bg_color: "0f172a",
theme_dark: "cobalt",
layout: "compact",
langs_count: "3",
exclude_repo: "repo-hidden",
size_weight: "0.5",
count_weight: "1",
role: "OWNER,COLLABORATOR",
disable_animations: "true",
stats_format: "bytes",
});
const req = {
headers: {},
url: `/api/top-langs?${params.toString()}`,
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
graphqlRequest: mock.history.post[0].data,
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public missing-username response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/top-langs",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should render error card in same theme as requested card", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/top-langs?theme=merko",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public blacklisted-username response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/top-langs?username=renovate-bot",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the private non-whitelisted username response snapshot", async () => {
vi.stubEnv("WHITELIST", "anuraghazra");
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/top-langs?username=martin-mfg",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the private missing-username response snapshot", async () => {
vi.stubEnv("WHITELIST", "anuraghazra");
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/top-langs",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
});
@@ -0,0 +1,235 @@
// @ts-check
import { logger } from "@stats-organization/github-readme-stats-core";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest";
import { normalizeSvg, wakaTimeData } from "../utils.js";
const mock = new MockAdapter(axios);
const wakaTimeProfileNotPublicData = {
data: {
viewer: {
gist: null,
},
},
};
const createResponse = () => ({
end: vi.fn(),
setHeader: vi.fn(),
});
beforeEach(() => {
vi.stubEnv("CACHE_SECONDS", "");
vi.stubEnv("GIST_WHITELIST", "");
vi.stubEnv("POSTGRES_URL", "");
vi.stubEnv("WHITELIST", "");
mock
.onGet(
"https://wakatime.com/api/v1/users/anuraghazra/stats?is_including_today=true",
)
.reply(200, wakaTimeData);
});
beforeAll(() => {
vi.spyOn(logger, "log").mockImplementation(() => {});
vi.spyOn(logger, "error").mockImplementation(() => {});
});
afterEach(() => {
mock.reset();
vi.unstubAllEnvs();
// modules may cache environment variables, so we need to reset them
vi.resetModules();
});
afterAll(() => {
vi.restoreAllMocks();
});
describe("Test /api/wakatime contract", () => {
it("should match the public happy-path response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/wakatime?username=anuraghazra",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public many-params response snapshot", async () => {
mock.reset(); // to verify api_domain param is working
mock
.onGet(
"https://wakatime.local/api/v1/users/anuraghazra/stats?is_including_today=true",
)
.reply(200, wakaTimeData);
const { default: router } = await import("../../router.js");
const params = new URLSearchParams({
username: "anuraghazra",
theme: "vue",
theme_light: "radical",
theme_dark: "cobalt",
title_color: "123456",
title_color_light: "ff3456",
title_color_dark: "ff6543",
text_color: "abcdef",
card_width: "620",
custom_title: "a custom title",
layout: "compact",
langs_count: "3",
hide: "other",
api_domain: "wakatime.local",
border_radius: "12",
border_color: "fedcba",
display_format: "percent",
disable_animations: "true",
});
const req = {
headers: {},
url: `/api/wakatime?${params.toString()}`,
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public missing-username response snapshot", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/wakatime",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should render error card in same theme as requested card", async () => {
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/wakatime?theme=merko",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the public inaccessible-profile response snapshot", async () => {
mock.reset();
mock
.onGet(
"https://wakatime.com/api/v1/users/anuraghazra/stats?is_including_today=true",
)
.reply(200, wakaTimeProfileNotPublicData);
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/wakatime?username=anuraghazra",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the private non-whitelisted username response snapshot", async () => {
vi.stubEnv("WHITELIST", "anuraghazra");
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/wakatime?username=martin-mfg",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
it("should match the private missing-username response snapshot", async () => {
vi.stubEnv("WHITELIST", "anuraghazra");
const { default: router } = await import("../../router.js");
const req = {
headers: {},
url: "/api/wakatime",
};
const res = createResponse();
await router(req, res);
expect(res.end).toHaveBeenCalledOnce();
expect({
headers: res.setHeader.mock.calls,
content: normalizeSvg(res.end.mock.calls[0][0]),
}).toMatchSnapshot();
});
});
-29
View File
@@ -1,29 +0,0 @@
// @ts-check
import { queryByTestId } from "@testing-library/dom";
import { describe, expect, it } from "vitest";
import { renderError } from "../src/common/render.js";
import "@testing-library/jest-dom/vitest";
describe("Test render.js", () => {
it("should test renderError", () => {
document.body.innerHTML = renderError({ message: "Something went wrong" });
expect(
queryByTestId(document.body, "message")?.children[0],
).toHaveTextContent(/Something went wrong/gim);
expect(
queryByTestId(document.body, "message")?.children[1],
).toBeEmptyDOMElement();
// Secondary message
document.body.innerHTML = renderError({
message: "Something went wrong",
secondaryMessage: "Secondary Message",
});
expect(
queryByTestId(document.body, "message")?.children[1],
).toHaveTextContent(/Secondary Message/gim);
});
});
-241
View File
@@ -1,241 +0,0 @@
import { queryByTestId } from "@testing-library/dom";
import { cssToObject } from "@uppercod/css-to-object";
import { describe, expect, it } from "vitest";
import { renderGistCard } from "../src/cards/gist.js";
import { themes } from "../themes/index.js";
import "@testing-library/jest-dom/vitest";
/**
* @type {import("../src/fetchers/gist").GistData}
*/
const data = {
name: "test",
nameWithOwner: "anuraghazra/test",
description: "Small test repository with different Python programs.",
language: "Python",
starsCount: 163,
forksCount: 19,
};
describe("test renderGistCard", () => {
it("should render correctly", () => {
document.body.innerHTML = renderGistCard(data);
const [header] = document.getElementsByClassName("header");
expect(header).toHaveTextContent("test");
expect(header).not.toHaveTextContent("anuraghazra");
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
"Small test repository with different Python programs.",
);
expect(queryByTestId(document.body, "starsCount")).toHaveTextContent("163");
expect(queryByTestId(document.body, "forksCount")).toHaveTextContent("19");
expect(queryByTestId(document.body, "lang-name")).toHaveTextContent(
"Python",
);
expect(queryByTestId(document.body, "lang-color")).toHaveAttribute(
"fill",
"#3572A5",
);
});
it("should display username in title if show_owner is true", () => {
document.body.innerHTML = renderGistCard(data, { show_owner: true });
const [header] = document.getElementsByClassName("header");
expect(header).toHaveTextContent("anuraghazra/test");
});
it("should trim header if name is too long", () => {
document.body.innerHTML = renderGistCard({
...data,
name: "some-really-long-repo-name-for-test-purposes",
});
const [header] = document.getElementsByClassName("header");
expect(header).toHaveTextContent("some-really-long-repo-name-for-test...");
});
it("should trim description if description os too long", () => {
document.body.innerHTML = renderGistCard({
...data,
description:
"The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
});
expect(
document.getElementsByClassName("description")[0].children[0].textContent,
).toBe("The quick brown fox jumps over the lazy dog is an");
expect(
document.getElementsByClassName("description")[0].children[1].textContent,
).toBe("English-language pangram—a sentence that contains all");
});
it("should not trim description if it is short", () => {
document.body.innerHTML = renderGistCard({
...data,
description: "Small text should not trim",
});
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
"Small text should not trim",
);
});
it("should render emojis in description", () => {
document.body.innerHTML = renderGistCard({
...data,
description: "This is a test gist description with :heart: emoji.",
});
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
"This is a test gist description with ❤️ emoji.",
);
});
it("should render custom colors properly", () => {
const customColors = {
title_color: "5a0",
icon_color: "1b998b",
text_color: "9991",
bg_color: "252525",
};
document.body.innerHTML = renderGistCard(data, {
...customColors,
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const descClassStyles = stylesObject[":host"][".description "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe(`#${customColors.title_color}`);
expect(descClassStyles.fill.trim()).toBe(`#${customColors.text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${customColors.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#252525",
);
});
it("should render with all the themes", () => {
Object.keys(themes).forEach((name) => {
document.body.innerHTML = renderGistCard(data, {
theme: name,
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const descClassStyles = stylesObject[":host"][".description "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe(
`#${themes[name].title_color}`,
);
expect(descClassStyles.fill.trim()).toBe(`#${themes[name].text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${themes[name].icon_color}`);
const backgroundElement = queryByTestId(document.body, "card-bg");
const backgroundElementFill = backgroundElement.getAttribute("fill");
expect([`#${themes[name].bg_color}`, "url(#gradient)"]).toContain(
backgroundElementFill,
);
});
});
it("should render custom colors with themes", () => {
document.body.innerHTML = renderGistCard(data, {
title_color: "5a0",
theme: "radical",
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const descClassStyles = stylesObject[":host"][".description "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe("#5a0");
expect(descClassStyles.fill.trim()).toBe(`#${themes.radical.text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${themes.radical.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
`#${themes.radical.bg_color}`,
);
});
it("should render custom colors with themes and fallback to default colors if invalid", () => {
document.body.innerHTML = renderGistCard(data, {
title_color: "invalid color",
text_color: "invalid color",
theme: "radical",
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const descClassStyles = stylesObject[":host"][".description "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe(
`#${themes.default.title_color}`,
);
expect(descClassStyles.fill.trim()).toBe(`#${themes.default.text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${themes.radical.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
`#${themes.radical.bg_color}`,
);
});
it("should not render star count or fork count if either of the are zero", () => {
document.body.innerHTML = renderGistCard({
...data,
starsCount: 0,
});
expect(queryByTestId(document.body, "starsCount")).toBeNull();
expect(queryByTestId(document.body, "forksCount")).toBeInTheDocument();
document.body.innerHTML = renderGistCard({
...data,
starsCount: 1,
forksCount: 0,
});
expect(queryByTestId(document.body, "starsCount")).toBeInTheDocument();
expect(queryByTestId(document.body, "forksCount")).toBeNull();
document.body.innerHTML = renderGistCard({
...data,
starsCount: 0,
forksCount: 0,
});
expect(queryByTestId(document.body, "starsCount")).toBeNull();
expect(queryByTestId(document.body, "forksCount")).toBeNull();
});
it("should render without rounding", () => {
document.body.innerHTML = renderGistCard(data, {
border_radius: "0",
});
expect(document.querySelector("rect")).toHaveAttribute("rx", "0");
document.body.innerHTML = renderGistCard(data, {});
expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
});
it("should fallback to default description", () => {
document.body.innerHTML = renderGistCard({
...data,
description: undefined,
});
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
"No description provided",
);
});
});
-373
View File
@@ -1,373 +0,0 @@
import { queryByTestId } from "@testing-library/dom";
import { cssToObject } from "@uppercod/css-to-object";
import { describe, expect, it } from "vitest";
import { renderRepoCard } from "../src/cards/repo.js";
import { themes } from "../themes/index.js";
import "@testing-library/jest-dom/vitest";
const data_repo = {
repository: {
nameWithOwner: "anuraghazra/convoychat",
name: "convoychat",
description: "Help us take over the world! React + TS + GraphQL Chat App",
primaryLanguage: {
color: "#2b7489",
id: "MDg6TGFuZ3VhZ2UyODc=",
name: "TypeScript",
},
starCount: 38000,
forkCount: 100,
},
};
describe("Test renderRepoCard", () => {
it("should render correctly", () => {
document.body.innerHTML = renderRepoCard(data_repo.repository);
const [header] = document.getElementsByClassName("header");
expect(header).toHaveTextContent("convoychat");
expect(header).not.toHaveTextContent("anuraghazra");
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
"Help us take over the world! React + TS + GraphQL Chat App",
);
expect(queryByTestId(document.body, "stargazers")).toHaveTextContent("38k");
expect(queryByTestId(document.body, "forkcount")).toHaveTextContent("100");
expect(queryByTestId(document.body, "lang-name")).toHaveTextContent(
"TypeScript",
);
expect(queryByTestId(document.body, "lang-color")).toHaveAttribute(
"fill",
"#2b7489",
);
});
it("should display username in title (full repo name)", () => {
document.body.innerHTML = renderRepoCard(data_repo.repository, {
show_owner: true,
});
expect(document.getElementsByClassName("header")[0]).toHaveTextContent(
"anuraghazra/convoychat",
);
});
it("should trim header", () => {
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
name: "some-really-long-repo-name-for-test-purposes",
});
expect(document.getElementsByClassName("header")[0].textContent).toBe(
"some-really-long-repo-name-for-test...",
);
});
it("should trim description", () => {
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
description:
"The quick brown fox jumps over the lazy dog is an English-language pangram—a sentence that contains all of the letters of the English alphabet",
});
expect(
document.getElementsByClassName("description")[0].children[0].textContent,
).toBe("The quick brown fox jumps over the lazy dog is an");
expect(
document.getElementsByClassName("description")[0].children[1].textContent,
).toBe("English-language pangram—a sentence that contains all");
// Should not trim
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
description: "Small text should not trim",
});
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
"Small text should not trim",
);
});
it("should render emojis", () => {
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
description: "This is a text with a :poop: poo emoji",
});
// poop emoji may not show in all editors but it's there between "a" and "poo"
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
"This is a text with a 💩 poo emoji",
);
});
it("should hide language if primaryLanguage is null & fallback to correct values", () => {
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
primaryLanguage: null,
});
expect(queryByTestId(document.body, "primary-lang")).toBeNull();
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
primaryLanguage: { color: null, name: null },
});
expect(queryByTestId(document.body, "primary-lang")).toBeInTheDocument();
expect(queryByTestId(document.body, "lang-color")).toHaveAttribute(
"fill",
"#333",
);
expect(queryByTestId(document.body, "lang-name")).toHaveTextContent(
"Unspecified",
);
});
it("should render default colors properly", () => {
document.body.innerHTML = renderRepoCard(data_repo.repository);
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const descClassStyles = stylesObject[":host"][".description "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe("#2f80ed");
expect(descClassStyles.fill.trim()).toBe("#434d58");
expect(iconClassStyles.fill.trim()).toBe("#586069");
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#fffefe",
);
});
it("should render custom colors properly", () => {
const customColors = {
title_color: "5a0",
icon_color: "1b998b",
text_color: "9991",
bg_color: "252525",
};
document.body.innerHTML = renderRepoCard(data_repo.repository, {
...customColors,
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const descClassStyles = stylesObject[":host"][".description "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe(`#${customColors.title_color}`);
expect(descClassStyles.fill.trim()).toBe(`#${customColors.text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${customColors.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#252525",
);
});
it("should render with all the themes", () => {
Object.keys(themes).forEach((name) => {
document.body.innerHTML = renderRepoCard(data_repo.repository, {
theme: name,
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const descClassStyles = stylesObject[":host"][".description "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe(
`#${themes[name].title_color}`,
);
expect(descClassStyles.fill.trim()).toBe(`#${themes[name].text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${themes[name].icon_color}`);
const backgroundElement = queryByTestId(document.body, "card-bg");
const backgroundElementFill = backgroundElement.getAttribute("fill");
expect([`#${themes[name].bg_color}`, "url(#gradient)"]).toContain(
backgroundElementFill,
);
});
});
it("should render custom colors with themes", () => {
document.body.innerHTML = renderRepoCard(data_repo.repository, {
title_color: "5a0",
theme: "radical",
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const descClassStyles = stylesObject[":host"][".description "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe("#5a0");
expect(descClassStyles.fill.trim()).toBe(`#${themes.radical.text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${themes.radical.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
`#${themes.radical.bg_color}`,
);
});
it("should render custom colors with themes and fallback to default colors if invalid", () => {
document.body.innerHTML = renderRepoCard(data_repo.repository, {
title_color: "invalid color",
text_color: "invalid color",
theme: "radical",
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const descClassStyles = stylesObject[":host"][".description "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe(
`#${themes.default.title_color}`,
);
expect(descClassStyles.fill.trim()).toBe(`#${themes.default.text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${themes.radical.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
`#${themes.radical.bg_color}`,
);
});
it("should not render star count or fork count if either of the are zero", () => {
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
starCount: 0,
});
expect(queryByTestId(document.body, "stargazers")).toBeNull();
expect(queryByTestId(document.body, "forkcount")).toBeInTheDocument();
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
starCount: 1,
forkCount: 0,
});
expect(queryByTestId(document.body, "stargazers")).toBeInTheDocument();
expect(queryByTestId(document.body, "forkcount")).toBeNull();
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
starCount: 0,
forkCount: 0,
});
expect(queryByTestId(document.body, "stargazers")).toBeNull();
expect(queryByTestId(document.body, "forkcount")).toBeNull();
});
it("should render badges", () => {
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
isArchived: true,
});
expect(queryByTestId(document.body, "badge")).toHaveTextContent("Archived");
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
isTemplate: true,
});
expect(queryByTestId(document.body, "badge")).toHaveTextContent("Template");
});
it("should not render template", () => {
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
});
expect(queryByTestId(document.body, "badge")).toBeNull();
});
it("should render translated badges", () => {
document.body.innerHTML = renderRepoCard(
{
...data_repo.repository,
isArchived: true,
},
{
locale: "cn",
},
);
expect(queryByTestId(document.body, "badge")).toHaveTextContent("已归档");
document.body.innerHTML = renderRepoCard(
{
...data_repo.repository,
isTemplate: true,
},
{
locale: "cn",
},
);
expect(queryByTestId(document.body, "badge")).toHaveTextContent("模板");
});
it("should render without rounding", () => {
document.body.innerHTML = renderRepoCard(data_repo.repository, {
border_radius: "0",
});
expect(document.querySelector("rect")).toHaveAttribute("rx", "0");
document.body.innerHTML = renderRepoCard(data_repo.repository, {});
expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
});
it("should fallback to default description", () => {
document.body.innerHTML = renderRepoCard({
...data_repo.repository,
description: undefined,
isArchived: true,
});
expect(document.getElementsByClassName("description")[0]).toHaveTextContent(
"No description provided",
);
});
it("should have correct height with specified `description_lines_count` parameter", () => {
// Testing short description
document.body.innerHTML = renderRepoCard(data_repo.repository, {
description_lines_count: 1,
});
expect(document.querySelector("svg")).toHaveAttribute("height", "120");
document.body.innerHTML = renderRepoCard(data_repo.repository, {
description_lines_count: 3,
});
expect(document.querySelector("svg")).toHaveAttribute("height", "150");
// Testing long description
const longDescription =
"A tool that will make a lot of iPhone/iPad developers' life easier. It shares your app over-the-air in a WiFi network. Bonjour is used and no configuration is needed.";
document.body.innerHTML = renderRepoCard(
{ ...data_repo.repository, description: longDescription },
{
description_lines_count: 3,
},
);
expect(document.querySelector("svg")).toHaveAttribute("height", "150");
document.body.innerHTML = renderRepoCard(
{ ...data_repo.repository, description: longDescription },
{
description_lines_count: 1,
},
);
expect(document.querySelector("svg")).toHaveAttribute("height", "120");
});
});
-468
View File
@@ -1,468 +0,0 @@
import {
getByTestId,
queryAllByTestId,
queryByTestId,
} from "@testing-library/dom";
import { cssToObject } from "@uppercod/css-to-object";
import { describe, expect, it } from "vitest";
import { renderStatsCard } from "../src/cards/stats.js";
import { CustomError } from "../src/common/error.js";
import { themes } from "../themes/index.js";
import "@testing-library/jest-dom/vitest";
const stats = {
name: "Anurag Hazra",
totalStars: 100,
totalCommits: 200,
totalIssues: 300,
totalPRs: 400,
totalPRsMerged: 320,
mergedPRsPercentage: 80,
totalReviews: 50,
totalDiscussionsStarted: 10,
totalDiscussionsAnswered: 50,
contributedTo: 500,
rank: { level: "A+", percentile: 40 },
};
describe("Test renderStatsCard", () => {
it("should render correctly", () => {
document.body.innerHTML = renderStatsCard(stats);
expect(document.getElementsByClassName("header")[0].textContent).toBe(
"Anurag Hazra's GitHub Stats",
);
expect(
document.body.getElementsByTagName("svg")[0].getAttribute("height"),
).toBe("195");
expect(getByTestId(document.body, "stars").textContent).toBe("100");
expect(getByTestId(document.body, "commits").textContent).toBe("200");
expect(getByTestId(document.body, "issues").textContent).toBe("300");
expect(getByTestId(document.body, "prs").textContent).toBe("400");
expect(getByTestId(document.body, "contribs").textContent).toBe("500");
expect(queryByTestId(document.body, "card-bg")).toBeInTheDocument();
expect(queryByTestId(document.body, "rank-circle")).toBeInTheDocument();
// Default hidden stats
expect(queryByTestId(document.body, "reviews")).not.toBeInTheDocument();
expect(
queryByTestId(document.body, "discussions_started"),
).not.toBeInTheDocument();
expect(
queryByTestId(document.body, "discussions_answered"),
).not.toBeInTheDocument();
expect(queryByTestId(document.body, "prs_merged")).not.toBeInTheDocument();
expect(
queryByTestId(document.body, "prs_merged_percentage"),
).not.toBeInTheDocument();
});
it("should have proper name apostrophe", () => {
document.body.innerHTML = renderStatsCard({ ...stats, name: "Anil Das" });
expect(document.getElementsByClassName("header")[0].textContent).toBe(
"Anil Das' GitHub Stats",
);
document.body.innerHTML = renderStatsCard({ ...stats, name: "Felix" });
expect(document.getElementsByClassName("header")[0].textContent).toBe(
"Felix's GitHub Stats",
);
});
it("should hide individual stats", () => {
document.body.innerHTML = renderStatsCard(stats, {
hide: ["issues", "prs", "contribs"],
});
expect(
document.body.getElementsByTagName("svg")[0].getAttribute("height"),
).toBe("150"); // height should be 150 because we clamped it.
expect(queryByTestId(document.body, "stars")).toBeDefined();
expect(queryByTestId(document.body, "commits")).toBeDefined();
expect(queryByTestId(document.body, "issues")).toBeNull();
expect(queryByTestId(document.body, "prs")).toBeNull();
expect(queryByTestId(document.body, "contribs")).toBeNull();
expect(queryByTestId(document.body, "reviews")).toBeNull();
expect(queryByTestId(document.body, "discussions_started")).toBeNull();
expect(queryByTestId(document.body, "discussions_answered")).toBeNull();
expect(queryByTestId(document.body, "prs_merged")).toBeNull();
expect(queryByTestId(document.body, "prs_merged_percentage")).toBeNull();
});
it("should show additional stats", () => {
document.body.innerHTML = renderStatsCard(stats, {
show: [
"reviews",
"discussions_started",
"discussions_answered",
"prs_merged",
"prs_merged_percentage",
],
});
expect(
document.body.getElementsByTagName("svg")[0].getAttribute("height"),
).toBe("320");
expect(queryByTestId(document.body, "stars")).toBeDefined();
expect(queryByTestId(document.body, "commits")).toBeDefined();
expect(queryByTestId(document.body, "issues")).toBeDefined();
expect(queryByTestId(document.body, "prs")).toBeDefined();
expect(queryByTestId(document.body, "contribs")).toBeDefined();
expect(queryByTestId(document.body, "reviews")).toBeDefined();
expect(queryByTestId(document.body, "discussions_started")).toBeDefined();
expect(queryByTestId(document.body, "discussions_answered")).toBeDefined();
expect(queryByTestId(document.body, "prs_merged")).toBeDefined();
expect(queryByTestId(document.body, "prs_merged_percentage")).toBeDefined();
});
it("should hide_rank", () => {
document.body.innerHTML = renderStatsCard(stats, { hide_rank: true });
expect(queryByTestId(document.body, "rank-circle")).not.toBeInTheDocument();
});
it("should render with custom width set", () => {
document.body.innerHTML = renderStatsCard(stats);
expect(document.querySelector("svg")).toHaveAttribute("width", "450");
document.body.innerHTML = renderStatsCard(stats, { card_width: 500 });
expect(document.querySelector("svg")).toHaveAttribute("width", "500");
});
it("should render with custom width set and limit minimum width", () => {
document.body.innerHTML = renderStatsCard(stats, { card_width: 1 });
expect(document.querySelector("svg")).toHaveAttribute("width", "1");
// Test default minimum card width without rank circle.
document.body.innerHTML = renderStatsCard(stats, {
card_width: 1,
hide_rank: true,
});
expect(document.querySelector("svg")).toHaveAttribute("width", "1");
// Test minimum card width with rank and icons.
document.body.innerHTML = renderStatsCard(stats, {
card_width: 1,
hide_rank: true,
show_icons: true,
});
expect(document.querySelector("svg")).toHaveAttribute("width", "1");
// Test minimum card width with icons but without rank.
document.body.innerHTML = renderStatsCard(stats, {
card_width: 1,
hide_rank: false,
show_icons: true,
});
expect(document.querySelector("svg")).toHaveAttribute("width", "1");
// Test minimum card width without icons or rank.
document.body.innerHTML = renderStatsCard(stats, {
card_width: 1,
hide_rank: false,
show_icons: false,
});
expect(document.querySelector("svg")).toHaveAttribute("width", "1");
});
it("should render default colors properly", () => {
document.body.innerHTML = renderStatsCard(stats);
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.textContent);
const headerClassStyles = stylesObject[":host"][".header "];
const statClassStyles = stylesObject[":host"][".stat "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe("#2f80ed");
expect(statClassStyles.fill.trim()).toBe("#434d58");
expect(iconClassStyles.fill.trim()).toBe("#4c71f2");
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#fffefe",
);
});
it("should render custom colors properly", () => {
const customColors = {
title_color: "5a0",
icon_color: "1b998b",
text_color: "9991",
bg_color: "252525",
};
document.body.innerHTML = renderStatsCard(stats, { ...customColors });
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const statClassStyles = stylesObject[":host"][".stat "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe(`#${customColors.title_color}`);
expect(statClassStyles.fill.trim()).toBe(`#${customColors.text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${customColors.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#252525",
);
});
it("should render custom colors with themes", () => {
document.body.innerHTML = renderStatsCard(stats, {
title_color: "5a0",
theme: "radical",
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const statClassStyles = stylesObject[":host"][".stat "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe("#5a0");
expect(statClassStyles.fill.trim()).toBe(`#${themes.radical.text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${themes.radical.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
`#${themes.radical.bg_color}`,
);
});
it("should render with all the themes", () => {
Object.keys(themes).forEach((name) => {
document.body.innerHTML = renderStatsCard(stats, {
theme: name,
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const statClassStyles = stylesObject[":host"][".stat "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe(
`#${themes[name].title_color}`,
);
expect(statClassStyles.fill.trim()).toBe(`#${themes[name].text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${themes[name].icon_color}`);
const backgroundElement = queryByTestId(document.body, "card-bg");
const backgroundElementFill = backgroundElement.getAttribute("fill");
expect([`#${themes[name].bg_color}`, "url(#gradient)"]).toContain(
backgroundElementFill,
);
});
});
it("should render custom colors with themes and fallback to default colors if invalid", () => {
document.body.innerHTML = renderStatsCard(stats, {
title_color: "invalid color",
text_color: "invalid color",
theme: "radical",
});
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const statClassStyles = stylesObject[":host"][".stat "];
const iconClassStyles = stylesObject[":host"][".icon "];
expect(headerClassStyles.fill.trim()).toBe(
`#${themes.default.title_color}`,
);
expect(statClassStyles.fill.trim()).toBe(`#${themes.default.text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${themes.radical.icon_color}`);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
`#${themes.radical.bg_color}`,
);
});
it("should render custom ring_color properly", () => {
const customColors = {
title_color: "5a0",
ring_color: "0000ff",
icon_color: "1b998b",
text_color: "9991",
bg_color: "252525",
};
document.body.innerHTML = renderStatsCard(stats, { ...customColors });
const styleTag = document.querySelector("style");
const stylesObject = cssToObject(styleTag.innerHTML);
const headerClassStyles = stylesObject[":host"][".header "];
const statClassStyles = stylesObject[":host"][".stat "];
const iconClassStyles = stylesObject[":host"][".icon "];
const rankCircleStyles = stylesObject[":host"][".rank-circle "];
const rankCircleRimStyles = stylesObject[":host"][".rank-circle-rim "];
expect(headerClassStyles.fill.trim()).toBe(`#${customColors.title_color}`);
expect(statClassStyles.fill.trim()).toBe(`#${customColors.text_color}`);
expect(iconClassStyles.fill.trim()).toBe(`#${customColors.icon_color}`);
expect(rankCircleStyles.stroke.trim()).toBe(`#${customColors.ring_color}`);
expect(rankCircleRimStyles.stroke.trim()).toBe(
`#${customColors.ring_color}`,
);
expect(queryByTestId(document.body, "card-bg")).toHaveAttribute(
"fill",
"#252525",
);
});
it("should render icons correctly", () => {
document.body.innerHTML = renderStatsCard(stats, {
show_icons: true,
});
expect(queryAllByTestId(document.body, "icon")[0]).toBeDefined();
expect(queryByTestId(document.body, "stars")).toBeDefined();
expect(
queryByTestId(document.body, "stars").previousElementSibling, // the label
).toHaveAttribute("x", "25");
});
it("should not have icons if show_icons is false", () => {
document.body.innerHTML = renderStatsCard(stats, { show_icons: false });
expect(queryAllByTestId(document.body, "icon")[0]).not.toBeDefined();
expect(queryByTestId(document.body, "stars")).toBeDefined();
expect(
queryByTestId(document.body, "stars").previousElementSibling, // the label
).not.toHaveAttribute("x");
});
it("should auto resize if hide_rank is true", () => {
document.body.innerHTML = renderStatsCard(stats, {
hide_rank: true,
});
expect(
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
).toBe("305.81250000000006");
});
it("should auto resize if hide_rank is true & custom_title is set", () => {
document.body.innerHTML = renderStatsCard(stats, {
hide_rank: true,
custom_title: "Hello world",
});
expect(
document.body.getElementsByTagName("svg")[0].getAttribute("width"),
).toBe("287");
});
it("should render translations", () => {
document.body.innerHTML = renderStatsCard(stats, { locale: "cn" });
expect(document.getElementsByClassName("header")[0].textContent).toBe(
"Anurag Hazra 的 GitHub 统计数据",
);
expect(
document.querySelector(
'g[transform="translate(0, 0)"]>.stagger>.stat.bold',
).textContent,
).toMatchInlineSnapshot(`"获标星数:"`);
expect(
document.querySelector(
'g[transform="translate(0, 25)"]>.stagger>.stat.bold',
).textContent,
).toMatchInlineSnapshot(`"累计提交总数 (去年):"`);
expect(
document.querySelector(
'g[transform="translate(0, 50)"]>.stagger>.stat.bold',
).textContent,
).toMatchInlineSnapshot(`"发起的 PR 总数:"`);
expect(
document.querySelector(
'g[transform="translate(0, 75)"]>.stagger>.stat.bold',
).textContent,
).toMatchInlineSnapshot(`"提出的 issue 总数:"`);
expect(
document.querySelector(
'g[transform="translate(0, 100)"]>.stagger>.stat.bold',
).textContent,
).toMatchInlineSnapshot(`"贡献的项目数(去年):"`);
});
it("should render without rounding", () => {
document.body.innerHTML = renderStatsCard(stats, { border_radius: "0" });
expect(document.querySelector("rect")).toHaveAttribute("rx", "0");
document.body.innerHTML = renderStatsCard(stats, {});
expect(document.querySelector("rect")).toHaveAttribute("rx", "4.5");
});
it("should shorten values", () => {
stats["totalCommits"] = 1999;
document.body.innerHTML = renderStatsCard(stats);
expect(getByTestId(document.body, "commits").textContent).toBe("2k");
document.body.innerHTML = renderStatsCard(stats, { number_format: "long" });
expect(getByTestId(document.body, "commits").textContent).toBe("1999");
document.body.innerHTML = renderStatsCard(stats, { number_precision: 2 });
expect(getByTestId(document.body, "commits").textContent).toBe("2.00k");
document.body.innerHTML = renderStatsCard(stats, {
number_format: "long",
number_precision: 2,
});
expect(getByTestId(document.body, "commits").textContent).toBe("1999");
});
it("should render default rank icon with level A+", () => {
document.body.innerHTML = renderStatsCard(stats, {
rank_icon: "default",
});
expect(queryByTestId(document.body, "level-rank-icon")).toBeDefined();
expect(
queryByTestId(document.body, "level-rank-icon").textContent.trim(),
).toBe("A+");
});
it("should render github rank icon", () => {
document.body.innerHTML = renderStatsCard(stats, {
rank_icon: "github",
});
expect(queryByTestId(document.body, "github-rank-icon")).toBeDefined();
});
it("should show the rank percentile", () => {
document.body.innerHTML = renderStatsCard(stats, {
rank_icon: "percentile",
});
expect(queryByTestId(document.body, "percentile-top-header")).toBeDefined();
expect(
queryByTestId(document.body, "percentile-top-header").textContent.trim(),
).toBe("Top");
expect(queryByTestId(document.body, "rank-percentile-text")).toBeDefined();
expect(
queryByTestId(document.body, "percentile-rank-value").textContent.trim(),
).toBe(stats.rank.percentile.toFixed(1) + "%");
});
it("should throw error if all stats and rank icon are hidden", () => {
expect(() =>
renderStatsCard(stats, {
hide: ["stars", "commits", "prs", "issues", "contribs"],
hide_rank: true,
}),
).toThrow(
new CustomError(
"Could not render stats card.",
"Either stats or rank are required.",
),
);
});
});
-85
View File
@@ -1,85 +0,0 @@
// @ts-check
import { describe, expect, it, vi } from "vitest";
import "@testing-library/jest-dom/vitest";
import { logger } from "../src/common/log.js";
import { retryer } from "../src/common/retryer.js";
const fetcher = vi.fn((variables, token) => {
logger.log(variables, token);
return new Promise((res) => res({ data: "ok" }));
});
const fetcherFail = vi.fn(() => {
return new Promise((res) =>
res({ data: { errors: [{ type: "RATE_LIMITED" }] } }),
);
});
const fetcherFailOnSecondTry = vi.fn((_vars, _token, retries) => {
return new Promise((res) => {
// faking rate limit
// @ts-ignore
if (retries < 1) {
return res({ data: { errors: [{ type: "RATE_LIMITED" }] } });
}
return res({ data: "ok" });
});
});
const fetcherFailWithMessageBasedRateLimitErr = vi.fn(
(_vars, _token, retries) => {
return new Promise((res) => {
// faking rate limit
// @ts-ignore
if (retries < 1) {
return res({
data: {
errors: [
{
type: "ASDF",
message: "API rate limit already exceeded for user ID 11111111",
},
],
},
});
}
return res({ data: "ok" });
});
},
);
describe("Test Retryer", () => {
it("retryer should return value and have zero retries on first try", async () => {
let res = await retryer(fetcher, {});
expect(fetcher).toHaveBeenCalledTimes(1);
expect(res).toStrictEqual({ data: "ok" });
});
it("retryer should return value and have 2 retries", async () => {
let res = await retryer(fetcherFailOnSecondTry, {});
expect(fetcherFailOnSecondTry).toHaveBeenCalledTimes(2);
expect(res).toStrictEqual({ data: "ok" });
});
it("retryer should return value and have 2 retries with message based rate limit error", async () => {
let res = await retryer(fetcherFailWithMessageBasedRateLimitErr, {});
expect(fetcherFailWithMessageBasedRateLimitErr).toHaveBeenCalledTimes(2);
expect(res).toStrictEqual({ data: "ok" });
});
it("retryer should throw specific error if maximum retries reached", async () => {
try {
await retryer(fetcherFail, {});
} catch (err) {
expect(fetcherFail).toHaveBeenCalledTimes(2);
// @ts-ignore
expect(err.message).toBe("Downtime due to GitHub API rate limiting");
}
});
});

Some files were not shown because too many files have changed in this diff Show More