diff --git a/apps/frontend/astro.config.ts b/apps/frontend/astro.config.ts
index 4e0191c38d3c1..06c2f5d529e04 100644
--- a/apps/frontend/astro.config.ts
+++ b/apps/frontend/astro.config.ts
@@ -98,7 +98,29 @@ export default defineConfig({
}),
],
vite: {
- plugins: [tailwindcss()],
+ plugins: [
+ tailwindcss(),
+
+ // The e2e run sets `STUB_CARD_API`: answer the card endpoints here rather than
+ // proxy them to a `pnpm dev:backend` that is not running.
+ !!process.env.STUB_CARD_API && {
+ name: "stub-card-api",
+ // In `configureServer`'s body, so it runs ahead of Vite's proxy middleware.
+ configureServer(server) {
+ server.middlewares.use((req, res, next) => {
+ const pathname = req.url?.split("?")[0] ?? "";
+ if (pathname !== "/api" && !pathname.startsWith("/api/")) {
+ next();
+ return;
+ }
+ res.setHeader("Content-Type", "image/svg+xml");
+ res.end(
+ '',
+ );
+ });
+ },
+ },
+ ],
/*
* On Vercel the same deployment serves `/api` and `/frontend`, so the docs can reference cards by root-relative path.
* Locally the two are separate servers, so forward `/api` to `pnpm dev:backend` to keep those paths working.
@@ -116,11 +138,5 @@ export default defineConfig({
},
],
},
- // The backend code the wizard reuses imports `pg`, which never runs in the browser.
- build: {
- rolldownOptions: {
- external: ["pg"],
- },
- },
},
});
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index 3c1bf9f115620..a4b0d1eeee596 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -16,7 +16,6 @@
"@astrojs/react": "^6.0.2",
"@astrojs/starlight": "^0.41.7",
"@reduxjs/toolkit": "^2.12.0",
- "@stats-organization/github-readme-stats-backend": "workspace:^",
"@stats-organization/github-readme-stats-core": "workspace:^",
"@tailwindcss/vite": "^4.3.3",
"astro": "^7.2.0",
diff --git a/apps/frontend/playwright.config.ts b/apps/frontend/playwright.config.ts
index 81684b84182e9..05f1ce21fb4cd 100644
--- a/apps/frontend/playwright.config.ts
+++ b/apps/frontend/playwright.config.ts
@@ -79,7 +79,12 @@ export default defineConfig({
command: "pnpm run dev",
url: baseURL,
reuseExistingServer: !process.env["CI"],
- // `astro dev` detaches itself in an AI-agent shell, which reads as exiting early.
- env: { ASTRO_DEV_BACKGROUND: "1" },
+ env: {
+ // `astro dev` detaches itself in an AI-agent shell, which reads as exiting early.
+ ASTRO_DEV_BACKGROUND: "1",
+ // `STUB_CARD_API` has the dev server answer `/api` itself, instead of proxying
+ // the card endpoints to a backend that is not running here.
+ STUB_CARD_API: "1",
+ },
},
});
diff --git a/apps/frontend/src/wizard/components/Card/SvgInline.tsx b/apps/frontend/src/wizard/components/Card/SvgInline.tsx
index c5cd9b47307a0..a42692132773c 100644
--- a/apps/frontend/src/wizard/components/Card/SvgInline.tsx
+++ b/apps/frontend/src/wizard/components/Card/SvgInline.tsx
@@ -1,5 +1,3 @@
-// @ts-expect-error type info should be added later
-import { router } from "@stats-organization/github-readme-stats-backend";
import { loadConfigFromEnv } from "@stats-organization/github-readme-stats-core";
import axios from "axios";
import { useEffect, useRef, useState } from "react";
@@ -12,7 +10,7 @@ import {
useIsAuthenticated,
useUserToken,
} from "../../../redux/selectors/userSelectors.js";
-import { createMockRequest, createMockResponse } from "../../mock-http.js";
+import { renderCard } from "../../renderCard.js";
interface SvgInlineProps {
url: string;
@@ -56,7 +54,6 @@ export function SvgInline(props: SvgInlineProps): JSX.Element {
setLoaded(false);
let body: string;
- let status;
if (isAuthenticated && (!userToken || userToken === "placeholderPAT")) {
// waiting for backend call to private-access
@@ -65,24 +62,13 @@ export function SvgInline(props: SvgInlineProps): JSX.Element {
if (stage === 4 && !isAuthenticated) {
const res = await axios.get(url);
+ if (res.status >= 300) {
+ console.error("failed to fetch SVG");
+ return;
+ }
body = res.data;
- status = res.status;
} else {
- const req = createMockRequest({
- method: "GET",
- url,
- });
- const res = createMockResponse();
- // will be solved by npm package
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
- await router(req, res);
- body = res._getBody() as string;
- status = res._getStatusCode();
- }
-
- if (status >= 300) {
- console.error("failed to fetch/generate SVG");
- return;
+ body = (await renderCard(url)).content;
}
if (!isCurrent) {
diff --git a/apps/frontend/src/wizard/mock-http.ts b/apps/frontend/src/wizard/mock-http.ts
deleted file mode 100644
index d62af77f2a1bb..0000000000000
--- a/apps/frontend/src/wizard/mock-http.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-type HeaderMap = Partial>;
-
-type CreateMockRequestOptions = {
- url: string;
- headers?: HeaderMap;
-} & (
- | {
- method: "GET";
- }
- | {
- method: "POST";
- body: unknown;
- }
-);
-
-type CreateMockRequestResult = {
- url: string;
- headers: HeaderMap;
-} & (
- | {
- method: "GET";
- }
- | {
- method: "POST";
- body: unknown;
- }
-);
-
-export function createMockRequest(
- options: CreateMockRequestOptions,
-): CreateMockRequestResult {
- const { headers = {}, ...rest } = options;
-
- return { ...rest, headers };
-}
-
-interface CreateMockResponseResult {
- statusCode: number;
- chunks: Array;
-
- setHeader(name: string, value: string): void;
- getHeader(name: string): string | undefined;
- getHeaders(): HeaderMap;
-
- write(chunk: unknown): void;
- end(chunk: unknown): void;
-
- // --- Helpers for inspection ---
- _getStatusCode(): number;
- _getHeaders(): HeaderMap;
- _getBody(): unknown;
-}
-
-export function createMockResponse(): CreateMockResponseResult {
- const statusCode = 200;
- const headers: HeaderMap = {};
- const chunks: Array = [];
-
- const res: CreateMockResponseResult = {
- statusCode,
- chunks,
-
- setHeader(name, value) {
- headers[name.toLowerCase()] = value;
- },
- getHeader(name) {
- return headers[name.toLowerCase()];
- },
- getHeaders() {
- return { ...headers };
- },
-
- write(chunk) {
- if (typeof chunk !== "string") {
- chunk = String(chunk);
- }
- chunks.push(chunk);
- },
-
- end(chunk) {
- res.write(chunk);
- },
-
- _getStatusCode() {
- return statusCode;
- },
- _getHeaders() {
- return { ...headers };
- },
- _getBody() {
- return chunks.join("");
- },
- };
-
- return res;
-}
diff --git a/apps/frontend/src/wizard/renderCard.ts b/apps/frontend/src/wizard/renderCard.ts
new file mode 100644
index 0000000000000..6e161bbf7dad1
--- /dev/null
+++ b/apps/frontend/src/wizard/renderCard.ts
@@ -0,0 +1,46 @@
+import {
+ api,
+ gist,
+ pin,
+ topLangs,
+ wakatime,
+} from "@stats-organization/github-readme-stats-core";
+
+/**
+ * What a core api handler returns:
+ * a rendered card, or a rendered error card.
+ */
+interface CardResult {
+ status: string;
+ content: string;
+}
+
+/** Core's api handlers are still JavaScript, so every param they destructure is inferred as required. */
+type CardHandler = (query: Record) => Promise;
+
+const CARD_HANDLERS: Record = {
+ "/api": api as CardHandler,
+ "/api/gist": gist as CardHandler,
+ "/api/pin": pin as CardHandler,
+ "/api/top-langs": topLangs as CardHandler,
+ "/api/wakatime": wakatime as CardHandler,
+};
+
+/**
+ * Renders a card in the browser from the URL the wizard built for it.
+ * The wizard's PAT arrives through `loadConfigFromEnv` instead.
+ *
+ * @param url - Absolute card URL, e.g. `https://host/api/top-langs?username=x`.
+ * @returns The rendered card, or the rendered error card when a param is rejected.
+ * @throws When `url` is not one of the card endpoints.
+ */
+export async function renderCard(url: string): Promise {
+ const { pathname, searchParams } = new URL(url);
+
+ const handler = CARD_HANDLERS[pathname];
+ if (!handler) {
+ throw new Error(`No card renderer for "${pathname}"`);
+ }
+
+ return handler(Object.fromEntries(searchParams));
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6fba5d942ac8c..cc848f3a4814c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -121,9 +121,6 @@ importers:
'@reduxjs/toolkit':
specifier: ^2.12.0
version: 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.8)(redux@5.0.1))(react@19.2.8)
- '@stats-organization/github-readme-stats-backend':
- specifier: workspace:^
- version: link:../backend
'@stats-organization/github-readme-stats-core':
specifier: workspace:^
version: link:../../packages/core