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