From 49d001f8ae41b94aa54aab71d2b085376ff49a7a Mon Sep 17 00:00:00 2001 From: Fernando Date: Sat, 10 Oct 2026 02:57:01 -0300 Subject: [PATCH] feat: add browser tests, a server function demo and Knip - Playwright tests run against both the SSR server and the SPA hosted under a sub-path like GitHub Pages (e2e/serve.ts builds and serves both). They fail on console errors, so hydration mismatches and broken deep links get caught. Runs in CI as a new "Browser tests" job. - /server demonstrates createServerFn with TanStack Query; the SPA build shows a notice instead of calling a server that doesn't exist. - Knip checks for unused files, exports and dependencies (`pnpm lint:unused`, part of `pnpm check` and CI). It flagged two exports in src/queries/posts.ts that are now private. - Dependabot skips minor bumps of oxc-transform-react, since @vitejs/plugin-react only accepts its 0.152.x range. Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 5 +- .github/workflows/ci.yml | 16 ++ .gitignore | 5 + AGENTS.md | 9 +- README.md | 59 +++--- e2e/serve.ts | 110 ++++++++++++ e2e/smoke.spec.ts | 99 +++++++++++ knip.json | 4 + package.json | 6 +- playwright.config.ts | 28 +++ pnpm-lock.yaml | 315 +++++++++++++++++++++++++++++++++ src/components/site-header.tsx | 3 + src/lib/env.ts | 2 + src/queries/posts.ts | 4 +- src/queries/server-info.ts | 8 + src/routeTree.gen.ts | 24 ++- src/routes/__root.tsx | 3 +- src/routes/index.tsx | 3 +- src/routes/server.tsx | 83 +++++++++ src/server/server-info.ts | 9 + 20 files changed, 758 insertions(+), 37 deletions(-) create mode 100644 e2e/serve.ts create mode 100644 e2e/smoke.spec.ts create mode 100644 knip.json create mode 100644 playwright.config.ts create mode 100644 src/lib/env.ts create mode 100644 src/queries/server-info.ts create mode 100644 src/routes/server.tsx create mode 100644 src/server/server-info.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f9d0ef2..789a3db 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,7 +13,6 @@ updates: patterns: ["oxlint", "oxlint-*", "oxfmt"] react: patterns: ["react", "react-dom", "@types/react", "@types/react-dom"] - # oxc-transform-react must stay in the range @vitejs/plugin-react expects. vite-and-vitest: patterns: ["vite", "@vitejs/*", "oxc-transform-react", "vitest", "@vitest/*"] @@ -24,6 +23,10 @@ updates: # Matches the Node major in .node-version; bump both together. - dependency-name: "@types/node" update-types: ["version-update:semver-major"] + # @vitejs/plugin-react pins a 0.x range (e.g. ^0.152.0 allows only 0.152.x). + # Bump it by hand when the plugin's peer range moves. + - dependency-name: "oxc-transform-react" + update-types: ["version-update:semver-minor"] - package-ecosystem: github-actions directory: / diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9b96cf..3eab748 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: - run: pnpm format:check - run: pnpm lint --deny-warnings - run: pnpm typecheck + - run: pnpm lint:unused - run: pnpm test build: @@ -34,3 +35,18 @@ jobs: - uses: actions/checkout@v7 - uses: ./.github/actions/setup - run: pnpm ${{ matrix.target == 'spa' && 'build:spa' || 'build' }} + + e2e: + name: Browser tests (SSR and SPA) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/setup + - run: pnpm exec playwright install --with-deps --only-shell chromium + - run: pnpm test:e2e + - if: failure() + uses: actions/upload-artifact@v7 + with: + name: playwright-traces + path: test-results/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index 29f6c8b..b9ca061 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,8 @@ coverage .idea .DS_Store *.sw? + +# Playwright +test-results +playwright-report +blob-report diff --git a/AGENTS.md b/AGENTS.md index fc0209c..aaa0f45 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,8 +6,10 @@ Package manager: pnpm. Node 24. ## Commands - `pnpm dev`: dev server with SSR on http://localhost:3000 (`pnpm dev:spa` for SPA mode) -- `pnpm check`: format check + lint (type-aware, warnings fail) + typecheck. Run before finishing. +- `pnpm check`: format check + lint (type-aware, warnings fail) + typecheck + Knip (unused + files, exports and dependencies). Run before finishing. - `pnpm test`: Vitest. `pnpm test:watch` for watch mode. +- `pnpm test:e2e`: builds both targets and runs the Playwright browser tests in `e2e/`. - `pnpm lint:fix` / `pnpm format`: auto-fix lint issues / format everything - `pnpm lint:react`: React Doctor scan (React anti-patterns, a11y, security) - `pnpm build` / `pnpm build:spa`: SSR build (`.output/`) / static SPA build (`dist/client/`) @@ -21,8 +23,9 @@ Package manager: pnpm. Node 24. the route `loader`, and read it with `useSuspenseQuery(options)` in the component. `ensureQueryData`/`fetchQuery`/`prefetchQuery` are deprecated. - The React Compiler is on: don't add `useMemo`, `useCallback` or `memo` for performance. -- Code must work in both modes. Server functions and server routes only work in the SSR - build; in the SPA build everything runs in the browser. +- Code must work in both modes. Server functions (`createServerFn`, kept in `src/server/`) + and server routes only work in the SSR build; check `isSpa` from `#/lib/env` and render a + fallback in the SPA build, like `src/routes/server.tsx` does. ## UI diff --git a/README.md b/README.md index cc704e3..672ee52 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ | Language | [TypeScript 7](https://www.typescriptlang.org/) (native compiler) | | UI | [Tailwind CSS 4](https://tailwindcss.com/), [shadcn/ui](https://ui.shadcn.com/) on [Base UI](https://base-ui.com/), [Lucide](https://lucide.dev/) icons | | Lint & format | [Oxlint](https://oxc.rs/docs/guide/usage/linter) (type-aware) + [Oxfmt](https://oxc.rs/docs/guide/usage/formatter), [@shadcn/lint](https://github.com/shadcn-ui/lint), [React Doctor](https://react.doctor/) | -| Tests | [Vitest 5](https://vitest.dev/), [Testing Library](https://testing-library.com/), jsdom | -| Tooling | [pnpm](https://pnpm.io/), Husky + lint-staged, GitHub Actions, Dependabot | +| Tests | [Vitest 5](https://vitest.dev/) + [Testing Library](https://testing-library.com/), [Playwright](https://playwright.dev/) browser tests for both builds | +| Tooling | [pnpm](https://pnpm.io/), [Knip](https://knip.dev/), Husky + lint-staged, GitHub Actions, Dependabot | ## ⚠️ Disclaimer @@ -45,13 +45,13 @@ pnpm dev # http://localhost:3000 TanStack Start renders on the server by default. Run the same app as a pure client-side SPA with the `spa` mode: -| | SSR server (default) | Static SPA | -| ---------------- | --------------------------------------------------------------- | -------------------------------------------- | -| Dev | `pnpm dev` | `pnpm dev:spa` | -| Build | `pnpm build` → `.output/` | `pnpm build:spa` → `dist/client/` | -| Run / deploy | `pnpm start`, or any [Nitro preset](https://nitro.build/deploy) | Any static host (GitHub Pages, S3, Netlify…) | -| Route loaders | Server on first load, then client | Always in the browser | -| Server functions | ✅ | ❌ (no server) | +| | SSR server (default) | Static SPA | +| --------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------- | +| Dev | `pnpm dev` | `pnpm dev:spa` | +| Build | `pnpm build` → `.output/` | `pnpm build:spa` → `dist/client/` | +| Run / deploy | `pnpm start`, or any [Nitro preset](https://nitro.build/deploy) | Any static host (GitHub Pages, S3, Netlify…) | +| Route loaders | Server on first load, then client | Always in the browser | +| [Server functions](src/server/server-info.ts) | ✅ | ❌ (no server) | - **Deploy the server** anywhere Nitro supports: `NITRO_PRESET=vercel pnpm build` (or `netlify`, `cloudflare`, `bun`, …). The default `node` preset runs with `pnpm start`. - **Deploy the SPA** under a sub-path by setting `BASE_PATH`, e.g. `BASE_PATH=/my-repo/ pnpm build:spa`. The build writes `index.html` plus a `404.html` copy so static hosts fall back to the app for client-side routes. @@ -59,34 +59,38 @@ TanStack Start renders on the server by default. Run the same app as a pure clie ## 📜 Scripts -| Command | Description | -| ----------------- | ------------------------------------------------------ | -| `pnpm dev` | Start the dev server (SSR) | -| `pnpm dev:spa` | Start the dev server in SPA mode | -| `pnpm build` | Build the SSR server with Nitro | -| `pnpm build:spa` | Build the static SPA | -| `pnpm start` | Run the SSR build | -| `pnpm preview` | Preview the SSR build (`pnpm preview:spa` for the SPA) | -| `pnpm test` | Run tests once (`pnpm test:watch` to watch) | -| `pnpm check` | Format check + lint + typecheck, the same as CI | -| `pnpm lint` | Lint with Oxlint (`pnpm lint:fix` to auto-fix) | -| `pnpm lint:react` | Scan for React issues with React Doctor | -| `pnpm format` | Format with Oxfmt (`pnpm format:check` to only check) | -| `pnpm typecheck` | Type-check with TypeScript 7 | +| Command | Description | +| ------------------ | ------------------------------------------------------- | +| `pnpm dev` | Start the dev server (SSR) | +| `pnpm dev:spa` | Start the dev server in SPA mode | +| `pnpm build` | Build the SSR server with Nitro | +| `pnpm build:spa` | Build the static SPA | +| `pnpm start` | Run the SSR build | +| `pnpm preview` | Preview the SSR build (`pnpm preview:spa` for the SPA) | +| `pnpm test` | Run tests once (`pnpm test:watch` to watch) | +| `pnpm test:e2e` | Build both targets and run the Playwright browser tests | +| `pnpm check` | Format check, lint, typecheck and unused-code check | +| `pnpm lint` | Lint with Oxlint (`pnpm lint:fix` to auto-fix) | +| `pnpm lint:react` | Scan for React issues with React Doctor | +| `pnpm lint:unused` | Find unused files, exports and dependencies with Knip | +| `pnpm format` | Format with Oxfmt (`pnpm format:check` to only check) | +| `pnpm typecheck` | Type-check with TypeScript 7 | ## 🗂️ Project structure ``` . ├── .github/ # CI, GitHub Pages deploy, React Doctor, Dependabot +├── e2e/ # Playwright browser tests for the SSR and SPA builds ├── public/ # Static files, served from BASE_PATH ├── scripts/setup.ts # Turns a clone into your own project ├── src/ │ ├── components/ │ │ └── ui/ # shadcn/ui components (yours to edit) -│ ├── lib/utils.ts # cn() class merging +│ ├── lib/ # cn() class merging, isSpa flag │ ├── queries/ # TanStack Query options │ ├── routes/ # File-based routes (__root.tsx is the HTML shell) +│ ├── server/ # Server functions (SSR build only) │ ├── test/ # Test setup and helpers │ ├── router.tsx # Router + QueryClient setup │ ├── routeTree.gen.ts # Generated by TanStack Router, don't edit @@ -94,8 +98,10 @@ TanStack Start renders on the server by default. Run the same app as a pure clie ├── AGENTS.md # Instructions for coding agents ├── components.json # shadcn/ui config ├── doctor.config.ts # React Doctor config +├── knip.json # Unused-code checker config ├── oxfmt.config.ts # Formatter config ├── oxlint.config.ts # Linter config +├── playwright.config.ts # Browser test config ├── vite.config.ts # Vite + Start + Nitro (SSR and SPA modes) └── vitest.config.ts # Test config ``` @@ -132,6 +138,7 @@ Everything runs on the [Oxc](https://oxc.rs/) toolchain, which is much faster th - **TanStack Query and Router** ESLint plugins run inside Oxlint as JS plugins. - **Oxfmt** formats code with Prettier-compatible output, and also sorts imports, Tailwind classes and `package.json`. - **React Doctor** checks for React anti-patterns, accessibility and security issues (`pnpm lint:react`), and comments on every PR through its GitHub Action. +- **Knip** finds unused files, exports and dependencies (`pnpm lint:unused`, part of `pnpm check`), the kind of leftovers refactors and coding agents leave behind. A pre-commit hook (Husky + lint-staged) fixes and formats staged files. In VS Code, install the recommended extensions (Oxc, TypeScript 7 and Tailwind CSS IntelliSense) to get format-on-save and inline lint errors. @@ -157,6 +164,10 @@ it("increments", async () => { }); ``` +### Browser tests + +`pnpm test:e2e` builds both targets and runs [Playwright](https://playwright.dev/) against each: the SSR server, and the SPA hosted under a sub-path the way GitHub Pages serves it. Every test also fails on console errors, so hydration mismatches and broken deep links get caught. Install the browser once with `pnpm exec playwright install chromium`. + ## 🤖 Working with coding agents - [`AGENTS.md`](AGENTS.md) explains the commands and conventions to agents (`CLAUDE.md` imports it). diff --git a/e2e/serve.ts b/e2e/serve.ts new file mode 100644 index 0000000..4499d4e --- /dev/null +++ b/e2e/serve.ts @@ -0,0 +1,110 @@ +// Builds both targets and serves them for the browser tests (started by playwright.config.ts): +// SSR: the Nitro server from .output/ +// SPA: dist/client/ under a sub-path, hosted like GitHub Pages (files first, +// then directory index.html, then 404.html with a 404 status) +// Pass --no-build to serve the existing build output. + +import { execFileSync, spawn } from "node:child_process"; +import { readFile, stat } from "node:fs/promises"; +import { + createServer, + type IncomingMessage, + type ServerResponse, +} from "node:http"; +import { extname, join, resolve } from "node:path"; +import { setTimeout as sleep } from "node:timers/promises"; + +const ssrPort = process.env.SSR_PORT ?? "4173"; +const spaPort = process.env.SPA_PORT ?? "4174"; +const basePath = process.env.SPA_BASE_PATH ?? "/react-ts-template/"; + +const vite = resolve("node_modules/vite/bin/vite.js"); + +if (!process.argv.includes("--no-build")) { + const { BASE_PATH: _, ...env } = process.env; + execFileSync(process.execPath, [vite, "build"], { stdio: "inherit", env }); + execFileSync(process.execPath, [vite, "build", "--mode", "spa"], { + stdio: "inherit", + env: { ...env, BASE_PATH: basePath }, + }); +} + +const ssr = spawn(process.execPath, [".output/server/index.mjs"], { + stdio: "inherit", + env: { ...process.env, PORT: ssrPort }, +}); +for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.on(signal, () => { + ssr.kill(); + process.exit(); + }); +} + +async function waitForServer(url: string, attemptsLeft = 100): Promise { + try { + await fetch(url); + } catch (error) { + if (attemptsLeft === 0) throw error; + await sleep(100); + return waitForServer(url, attemptsLeft - 1); + } +} + +// Playwright waits for the SPA URL, so only start that server once SSR is up. +await waitForServer(`http://localhost:${ssrPort}/`); + +const contentTypes: Record = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript", + ".css": "text/css", + ".svg": "image/svg+xml", + ".woff2": "font/woff2", +}; + +async function isFile(path: string) { + try { + return (await stat(path)).isFile(); + } catch { + return false; + } +} + +const root = resolve("dist/client"); + +async function serveSpa(request: IncomingMessage, response: ServerResponse) { + const { pathname } = new URL(request.url ?? "/", "http://localhost"); + if (!pathname.startsWith(basePath)) { + response.writeHead(404).end(); + return; + } + + let file = join(root, decodeURIComponent(pathname.slice(basePath.length))); + if (!file.startsWith(root)) { + response.writeHead(400).end(); + return; + } + + let status = 200; + if (!(await isFile(file))) { + if (await isFile(join(file, "index.html"))) { + file = join(file, "index.html"); + } else { + status = 404; + file = join(root, "404.html"); + } + } + + response.writeHead(status, { + "content-type": contentTypes[extname(file)] ?? "application/octet-stream", + }); + response.end(await readFile(file)); +} + +createServer((request, response) => { + serveSpa(request, response).catch((error: unknown) => { + console.error(error); + response.writeHead(500).end(); + }); +}).listen(Number(spaPort), () => { + console.info(`SPA: http://localhost:${spaPort}${basePath}`); +}); diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts new file mode 100644 index 0000000..4c2c0fe --- /dev/null +++ b/e2e/smoke.spec.ts @@ -0,0 +1,99 @@ +import { expect, test } from "@playwright/test"; + +// Every test also fails on uncaught errors and console errors, which is how +// React reports hydration mismatches. +let errors: string[] = []; + +test.beforeEach(({ page }) => { + errors = []; + page.on("pageerror", (error) => errors.push(error.message)); + page.on("console", (message) => { + if (message.type() !== "error") return; + // Not-found pages and SPA deep links (served from 404.html) load with a 404 status. + const isDocument404 = + message.text().includes("404") && message.location().url === page.url(); + if (!isDocument404) errors.push(message.text()); + }); +}); + +test.afterEach(() => { + expect(errors).toEqual([]); +}); + +test("renders the home page", async ({ page }, testInfo) => { + await page.goto("./"); + + await expect( + page.getByRole("heading", { name: "ships as an SPA or a server" }), + ).toBeVisible(); + await expect( + page.getByText( + testInfo.project.name === "spa" + ? "Rendering as a static SPA" + : "Rendering as server-side (SSR)", + ), + ).toBeVisible(); +}); + +test("navigates to the demo and loads posts", async ({ page }) => { + await page.goto("./"); + await page.getByRole("link", { name: "Try the data demo" }).click(); + + await expect( + page.getByRole("heading", { name: "TanStack Router + Query" }), + ).toBeVisible(); + await expect(page.getByRole("listitem").first()).toBeVisible(); +}); + +test("opens a deep link with the right nav link active", async ({ page }) => { + await page.goto("demo"); + + await expect( + page.getByRole("heading", { name: "TanStack Router + Query" }), + ).toBeVisible(); + const nav = page.getByRole("navigation"); + await expect(nav.getByRole("link", { name: "Demo" })).toHaveAttribute( + "aria-current", + "page", + ); + await expect(nav.getByRole("link", { name: "Home" })).not.toHaveAttribute( + "aria-current", + ); +}); + +test("shows the not found page", async ({ page }) => { + await page.goto("this-page-does-not-exist"); + + await expect( + page.getByRole("heading", { name: "Page not found" }), + ).toBeVisible(); +}); + +test("calls a server function, or explains why it can't", async ({ + page, +}, testInfo) => { + await page.goto("server"); + + if (testInfo.project.name === "spa") { + await expect( + page.getByText("Server functions need the SSR build"), + ).toBeVisible(); + return; + } + + await expect(page.getByText("Hello from the server!")).toBeVisible(); + const time = page.locator("time"); + const firstAnswer = await time.getAttribute("datetime"); + await page.getByRole("button", { name: "Call it again" }).click(); + await expect(time).not.toHaveAttribute("datetime", firstAnswer ?? ""); +}); + +test.describe("dark mode", () => { + test.use({ colorScheme: "dark" }); + + test("follows the system color scheme", async ({ page }) => { + await page.goto("./"); + + await expect(page.locator("html")).toHaveClass(/\bdark\b/); + }); +}); diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..7ecb9e0 --- /dev/null +++ b/knip.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "entry": ["scripts/*.ts", "doctor.config.ts", "src/components/ui/*.tsx"] +} diff --git a/package.json b/package.json index 0f1bb75..aed3851 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,15 @@ "preview:spa": "vite preview --mode spa", "test": "vitest run", "test:watch": "vitest", + "test:e2e": "playwright test", "lint": "oxlint", "lint:fix": "oxlint --fix", "lint:react": "react-doctor . --verbose", + "lint:unused": "knip", "format": "oxfmt", "format:check": "oxfmt --check", "typecheck": "tsc --noEmit", - "check": "oxfmt --check && oxlint --deny-warnings && tsc --noEmit", + "check": "oxfmt --check && oxlint --deny-warnings && tsc --noEmit && knip", "prepare": "husky" }, "dependencies": { @@ -40,6 +42,7 @@ "react-dom": "^19.3.0" }, "devDependencies": { + "@playwright/test": "^1.64.0", "@shadcn/lint": "^0.2.0", "@tailwindcss/vite": "^4.3.3", "@tanstack/devtools-vite": "^0.8.5", @@ -58,6 +61,7 @@ "@vitejs/plugin-react": "^6.1.2", "husky": "^9.1.7", "jsdom": "^30.1.2", + "knip": "^6.40.0", "lint-staged": "^17.6.0", "nitro": "3.0.260903-beta", "oxc-transform-react": "~0.152.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..b595fc1 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,28 @@ +import { defineConfig, devices } from "@playwright/test"; + +// Both builds are served by e2e/serve.ts. Tests navigate with relative paths +// ("demo", not "/demo") so the SPA's sub-path is kept. +const ssrURL = "http://localhost:4173/"; +const spaURL = "http://localhost:4174/react-ts-template/"; + +export default defineConfig({ + testDir: "e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + reporter: process.env.CI ? [["github"], ["list"]] : "list", + use: { + ...devices["Desktop Chrome"], + trace: "retain-on-failure", + }, + projects: [ + { name: "ssr", use: { baseURL: ssrURL } }, + { name: "spa", use: { baseURL: spaURL } }, + ], + webServer: { + command: "node e2e/serve.ts", + url: spaURL, + reuseExistingServer: !process.env.CI, + timeout: 180_000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8cd5d2..1356037 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,6 +200,9 @@ importers: specifier: ^19.3.0 version: 19.3.0(react@19.3.0) devDependencies: + '@playwright/test': + specifier: ^1.64.0 + version: 1.64.0 '@shadcn/lint': specifier: ^0.2.0 version: 0.2.0(eslint@10.12.0(jiti@2.7.0)) @@ -254,6 +257,9 @@ importers: jsdom: specifier: ^30.1.2 version: 30.1.2 + knip: + specifier: ^6.40.0 + version: 6.40.0 lint-staged: specifier: ^17.6.0 version: 17.6.0 @@ -773,6 +779,12 @@ packages: cpu: [arm] os: [android] + '@oxc-parser/binding-android-arm-eabi@0.150.0': + resolution: {integrity: sha512-oQef2Zu4Prz1KLKznz3HqZzU9uVoA5PMoDZuuLmqms7hKmKSAPzlaMnLllJq3t+rgKmfJJ2siPrpZfFrW06btw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + '@oxc-parser/binding-android-arm64@0.120.0': resolution: {integrity: sha512-SEf80EHdhlbjZEgzeWm0ZA/br4GKMenDW3QB/gtyeTV1gStvvZeFi40ioHDZvds2m4Z9J1bUAUL8yn1/+A6iGg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -785,6 +797,12 @@ packages: cpu: [arm64] os: [android] + '@oxc-parser/binding-android-arm64@0.150.0': + resolution: {integrity: sha512-B6ofpoFiAUwIZ0MJ2IgHPvZK8FAtL4qzSZRwOkAKIfxEobE1mQC8nDdiFZj7pUaJiVUVCsfkMsBJKedzO8rZvA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@oxc-parser/binding-darwin-arm64@0.120.0': resolution: {integrity: sha512-xVrrbCai8R8CUIBu3CjryutQnEYhZqs1maIqDvtUCFZb8vY33H7uh9mHpL3a0JBIKoBUKjPH8+rzyAeXnS2d6A==} engines: {node: ^20.19.0 || >=22.12.0} @@ -797,6 +815,12 @@ packages: cpu: [arm64] os: [darwin] + '@oxc-parser/binding-darwin-arm64@0.150.0': + resolution: {integrity: sha512-J+9IHKzx/bSz1JetOfD4zKXSK9sOm4/a7+0qomJODcTL6JsRXGeV/ZdkAPSIDydfFmWzYCraMlQEosSZEyBYkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@oxc-parser/binding-darwin-x64@0.120.0': resolution: {integrity: sha512-xyHBbnJ6mydnQUH7MAcafOkkrNzQC6T+LXgDH/3InEq2BWl/g424IMRiJVSpVqGjB+p2bd0h0WRR8iIwzjU7rw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -809,6 +833,12 @@ packages: cpu: [x64] os: [darwin] + '@oxc-parser/binding-darwin-x64@0.150.0': + resolution: {integrity: sha512-v6IPfcAcSYrBWXV5Tce1DmxDMXLhEYpIIWiRFPJopgCVscZdLaV4MRQOUxvL3usQlw+yrwYOjBFB9IwBx+jUiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@oxc-parser/binding-freebsd-x64@0.120.0': resolution: {integrity: sha512-UMnVRllquXUYTeNfFKmxTTEdZ/ix1nLl0ducDzMSREoWYGVIHnOOxoKMWlCOvRr9Wk/HZqo2rh1jeumbPGPV9A==} engines: {node: ^20.19.0 || >=22.12.0} @@ -821,6 +851,12 @@ packages: cpu: [x64] os: [freebsd] + '@oxc-parser/binding-freebsd-x64@0.150.0': + resolution: {integrity: sha512-AoR/4jD02HET0KO0yNT4nbTE+XJUxiO9jB1X/ycEjUE3WrIJ3IjV/Vf+gskhWLcqqeXfvNnkDtBR7epllAm88A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@oxc-parser/binding-linux-arm-gnueabihf@0.120.0': resolution: {integrity: sha512-tkvn2CQ7QdcsMnpfiX3fd3wA3EFsWKYlcQzq9cFw/xc89Al7W6Y4O0FgLVkVQpo0Tnq/qtE1XfkJOnRRA9S/NA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -833,6 +869,12 @@ packages: cpu: [arm] os: [linux] + '@oxc-parser/binding-linux-arm-gnueabihf@0.150.0': + resolution: {integrity: sha512-A/hycCFjLUrCmLoL5O/vBp40ohlaXO1Ta3v5hqYZxicYFs129wZmgZJggcW4mYGYbZebQLhup+N8JjeQl8qwyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxc-parser/binding-linux-arm-musleabihf@0.120.0': resolution: {integrity: sha512-WN5y135Ic42gQDk9grbwY9++fDhqf8knN6fnP+0WALlAUh4odY/BDK1nfTJRSfpJD9P3r1BwU0m3pW2DU89whQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -845,6 +887,12 @@ packages: cpu: [arm] os: [linux] + '@oxc-parser/binding-linux-arm-musleabihf@0.150.0': + resolution: {integrity: sha512-pbqahg1Pkz7J4RKmXm30s/iQu99hv64ayXCu8P4p75HcEH5azOdR4eGqiB6lFGkFR3mMpzaYsSKLkS8oJbjmeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxc-parser/binding-linux-arm64-gnu@0.120.0': resolution: {integrity: sha512-1GgQBCcXvFMw99EPdMy+4NZ3aYyXsxjf9kbUUg8HuAy3ZBXzOry5KfFEzT9nqmgZI1cuetvApkiJBZLAPo8uaw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -859,6 +907,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-arm64-gnu@0.150.0': + resolution: {integrity: sha512-HV11aRbBQGwqv8bEo+K/6qr88uB4fEe1mhXncMhlVCrj+WpBSraxrUzHaPVmeQRU6x6x8v/3DuCbbo93rpp+fw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-arm64-musl@0.120.0': resolution: {integrity: sha512-gmMQ70gsPdDBgpcErvJEoWNBr7bJooSLlvOBVBSGfOzlP5NvJ3bFvnUeZZ9d+dPrqSngtonf7nyzWUTUj/U+lw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -873,6 +928,13 @@ packages: os: [linux] libc: [musl] + '@oxc-parser/binding-linux-arm64-musl@0.150.0': + resolution: {integrity: sha512-k6pVkJqALwtEuP4zukVhGRhdIy4+ofChUIUUsAHHyqDZlNsknmel3JjbggrJiWGaes6h/dIcWmEXsKW4SmK9oQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@oxc-parser/binding-linux-ppc64-gnu@0.120.0': resolution: {integrity: sha512-T/kZuU0ajop0xhzVMwH5r3srC9Nqup5HaIo+3uFjIN5uPxa0LvSxC1ZqP4aQGJVW5G0z8/nCkjIfSMS91P/wzw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -887,6 +949,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-ppc64-gnu@0.150.0': + resolution: {integrity: sha512-tEFg39mw/rHO5n8GK2DR4ZMFfsPQZtnQIPbsIpjoQRomJc/2tRfsCSmCT6gNit+NZGoeJG5aH9ATDH5bf0WnoQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-riscv64-gnu@0.120.0': resolution: {integrity: sha512-vn21KXLAXzaI3N5CZWlBr1iWeXLl9QFIMor7S1hUjUGTeUuWCoE6JZB040/ZNDwf+JXPX8Ao9KbmJq9FMC2iGw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -901,6 +970,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-riscv64-gnu@0.150.0': + resolution: {integrity: sha512-0JO7IFkoek6HqV589Menx3hJySNXi6quRhO4tq2P7kpEHKEXoDATnoPVUpn5B7gDH2KLkdo751N0uIrGJFCTCw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-riscv64-musl@0.120.0': resolution: {integrity: sha512-SUbUxlar007LTGmSLGIC5x/WJvwhdX+PwNzFJ9f/nOzZOrCFbOT4ikt7pJIRg1tXVsEfzk5mWpGO1NFiSs4PIw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -915,6 +991,13 @@ packages: os: [linux] libc: [musl] + '@oxc-parser/binding-linux-riscv64-musl@0.150.0': + resolution: {integrity: sha512-7XPzREnyAS5wHU9aB+49Uaqgoo1gVCklHc3DRlc3fJy2tXD/eAJFN1QFz/crj+Ulup5P1+axNREF+/RGaB/IRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + '@oxc-parser/binding-linux-s390x-gnu@0.120.0': resolution: {integrity: sha512-hYiPJTxyfJY2+lMBFk3p2bo0R9GN+TtpPFlRqVchL1qvLG+pznstramHNvJlw9AjaoRUHwp9IKR7UZQnRPGjgQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -929,6 +1012,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-s390x-gnu@0.150.0': + resolution: {integrity: sha512-7n7ZxcbRWDFaTdyj8M8p0O9OfzoMKm8O6syBAIgcSutbOALq0Bb9G52Me+XH0anMLZV+NgXc726gqgG2q39vcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-x64-gnu@0.120.0': resolution: {integrity: sha512-q+5jSVZkprJCIy3dzJpApat0InJaoxQLsJuD6DkX8hrUS61z2lHQ1Fe9L2+TYbKHXCLWbL0zXe7ovkIdopBGMQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -943,6 +1033,13 @@ packages: os: [linux] libc: [glibc] + '@oxc-parser/binding-linux-x64-gnu@0.150.0': + resolution: {integrity: sha512-Vx0GSA9ZCRTUiczoEQnIIyXkMvtEY8uc6IiwLQtMe5ci2sXPqjrry0Ek5fsPP1k2kCzkML3ow9UOOEgnEDi7Bw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@oxc-parser/binding-linux-x64-musl@0.120.0': resolution: {integrity: sha512-D9QDDZNnH24e7X4ftSa6ar/2hCavETfW3uk0zgcMIrZNy459O5deTbWrjGzZiVrSWigGtlQwzs2McBP0QsfV1w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -957,6 +1054,13 @@ packages: os: [linux] libc: [musl] + '@oxc-parser/binding-linux-x64-musl@0.150.0': + resolution: {integrity: sha512-ii4/9m3viDLssMnfXU7/Pni/3nYplApuGayceX4qsVGaqqQJCx3tHIH9M/HWIeSty5i8LjS21zPYT6lVIkwLxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@oxc-parser/binding-openharmony-arm64@0.120.0': resolution: {integrity: sha512-TBU8ZwOUWAOUWVfmI16CYWbvh4uQb9zHnGBHsw5Cp2JUVG044OIY1CSHODLifqzQIMTXvDvLzcL89GGdUIqNrA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -969,6 +1073,12 @@ packages: cpu: [arm64] os: [openharmony] + '@oxc-parser/binding-openharmony-arm64@0.150.0': + resolution: {integrity: sha512-ZtoxX5rez36ZWkwtiEhjkCPnlTPoQ2I7lIL0IYZ1yjxMYohbvoOjBmUIZzrf9W/HouONq0omIfTeCWEY+R380w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@oxc-parser/binding-wasm32-wasi@0.120.0': resolution: {integrity: sha512-WG/FOZgDJCpJnuF3ToG/K28rcOmSY7FmFmfBKYb2fmLyhDzPpUldFGV7/Fz4ru0Iz/v4KPmf8xVgO8N3lO4KHA==} engines: {node: '>=14.0.0'} @@ -986,6 +1096,12 @@ packages: cpu: [arm64] os: [win32] + '@oxc-parser/binding-win32-arm64-msvc@0.150.0': + resolution: {integrity: sha512-VqeRb5JX/bKYBrff7TUDvJyKY0914UzuCkuXIGxJS4FUmvMNfqkCbc7Sq90iMz9DlmAtlggwaBYQ9eg3h3ltiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@oxc-parser/binding-win32-ia32-msvc@0.120.0': resolution: {integrity: sha512-L7vfLzbOXsjBXV0rv/6Y3Jd9BRjPeCivINZAqrSyAOZN3moCopDN+Psq9ZrGNZtJzP8946MtlRFZ0Als0wBCOw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -998,6 +1114,12 @@ packages: cpu: [ia32] os: [win32] + '@oxc-parser/binding-win32-ia32-msvc@0.150.0': + resolution: {integrity: sha512-EPqJfeZ4Pgg2BJSsCV8GozxV0FDltRzpVtGVa1r2noJu1iu+opOw3gtBWXwIo9odCT/MdFfyHxdy0/CRqs3OqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + '@oxc-parser/binding-win32-x64-msvc@0.120.0': resolution: {integrity: sha512-ys+upfqNtSu58huAhJMBKl3XCkGzyVFBlMlGPzHeFKgpFF/OdgNs1MMf8oaJIbgMH8ZxgGF7qfue39eJohmKIg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1010,12 +1132,21 @@ packages: cpu: [x64] os: [win32] + '@oxc-parser/binding-win32-x64-msvc@0.150.0': + resolution: {integrity: sha512-n5YMzbqwPQqozbkrexi0lNZrUz5cnPHalYpFWe6Dau7AmKIWNo+y24IwzDuZEQtEdUHuKhXmMMih/MPjOI+CMw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@oxc-project/types@0.120.0': resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} '@oxc-project/types@0.148.0': resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} + '@oxc-project/types@0.150.0': + resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==} + '@oxc-project/types@0.153.0': resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} @@ -1640,6 +1771,11 @@ packages: cpu: [x64] os: [win32] + '@playwright/test@1.64.0': + resolution: {integrity: sha512-oTN9FNYuN6bqQMwiho6jJKLCG2/QOYCixGXuCjRmzOq23eODeukuY2XiAVoVpAECbuezMt1MHQjAiQxK8F+Y+g==} + engines: {node: '>=20'} + hasBin: true + '@rolldown/binding-android-arm-eabi@1.2.13': resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3055,6 +3191,9 @@ packages: fastq@1.20.3: resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -3096,6 +3235,11 @@ packages: flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + formatly@0.7.1: + resolution: {integrity: sha512-ESuCEJE0SGgabl9q+zABI8LhJDmF+8nhpuqDzuM48HfJ9QTD5o6PqFJDFZY7w6f8xsh5lixuj5nJsQNvYOWvKQ==} + engines: {node: '>=18.3.0'} + hasBin: true + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3147,6 +3291,9 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -3433,6 +3580,11 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + knip@6.40.0: + resolution: {integrity: sha512-5Lp/ANJu3AkW5f+J+2QG14z7xKZGi/ybb5MU1EWjeGdt6MpuIaLpLFiyBi0ZKryTiuTHiJqo/WKJRaWYimvavw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + launch-editor@2.14.2: resolution: {integrity: sha512-NHJEb30MGBCkmZXLZwz25t37QR4aY03ZzPYUJXOJaOa92uWwExI9SBQUuIznBYc1xkD7cJNGX7oc3Nl3sq2kQQ==} @@ -3787,6 +3939,10 @@ packages: resolution: {integrity: sha512-syxUKHeUll89RIABQADcI7sikYrwyssvA6gj4phSSIPezKVM8yMaLAiLLSc7fmzVvrwybfFGFbW5zme9sX87rg==} engines: {node: ^20.19.0 || >=22.12.0} + oxc-parser@0.150.0: + resolution: {integrity: sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg==} + engines: {node: ^20.19.0 || >=22.12.0} + oxc-resolver@11.24.2: resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} @@ -3861,6 +4017,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-manager-detector@1.9.0: + resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -3921,6 +4080,11 @@ packages: engines: {node: '>=20'} hasBin: true + playwright@1.64.0: + resolution: {integrity: sha512-kVzTnYFYEyQ9RGzBjNCd2+yEqWmVbxl3AHPynv3qQ6JTjv4a+kwRcnqQwIpuP3NnrnIrE/Q6wWAIAvMfIE2/Yg==} + engines: {node: '>=20'} + hasBin: true + postcss-selector-parser@7.1.6: resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} engines: {node: '>=4'} @@ -4024,6 +4188,9 @@ packages: reselect@5.3.0: resolution: {integrity: sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -4145,6 +4312,10 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + smol-toml@1.9.0: + resolution: {integrity: sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==} + engines: {node: '>= 18'} + socks@2.8.10: resolution: {integrity: sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} @@ -4237,6 +4408,10 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + stubborn-fs@2.0.0: resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} @@ -4358,6 +4533,10 @@ packages: resolution: {integrity: sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==} engines: {node: '>=18'} + unbash@4.0.12: + resolution: {integrity: sha512-PnKvRX+Fbxl/usVlTr/31tXIOVAH+8OdC9qaxUQcvurwEnoc3Vk/ipi2Ce8IjMxzK9byfSGdsUBjqqJGc1o5lw==} + engines: {node: '>=14'} + undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} @@ -4544,6 +4723,10 @@ packages: resolution: {integrity: sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} @@ -5209,96 +5392,144 @@ snapshots: '@oxc-parser/binding-android-arm-eabi@0.148.0': optional: true + '@oxc-parser/binding-android-arm-eabi@0.150.0': + optional: true + '@oxc-parser/binding-android-arm64@0.120.0': optional: true '@oxc-parser/binding-android-arm64@0.148.0': optional: true + '@oxc-parser/binding-android-arm64@0.150.0': + optional: true + '@oxc-parser/binding-darwin-arm64@0.120.0': optional: true '@oxc-parser/binding-darwin-arm64@0.148.0': optional: true + '@oxc-parser/binding-darwin-arm64@0.150.0': + optional: true + '@oxc-parser/binding-darwin-x64@0.120.0': optional: true '@oxc-parser/binding-darwin-x64@0.148.0': optional: true + '@oxc-parser/binding-darwin-x64@0.150.0': + optional: true + '@oxc-parser/binding-freebsd-x64@0.120.0': optional: true '@oxc-parser/binding-freebsd-x64@0.148.0': optional: true + '@oxc-parser/binding-freebsd-x64@0.150.0': + optional: true + '@oxc-parser/binding-linux-arm-gnueabihf@0.120.0': optional: true '@oxc-parser/binding-linux-arm-gnueabihf@0.148.0': optional: true + '@oxc-parser/binding-linux-arm-gnueabihf@0.150.0': + optional: true + '@oxc-parser/binding-linux-arm-musleabihf@0.120.0': optional: true '@oxc-parser/binding-linux-arm-musleabihf@0.148.0': optional: true + '@oxc-parser/binding-linux-arm-musleabihf@0.150.0': + optional: true + '@oxc-parser/binding-linux-arm64-gnu@0.120.0': optional: true '@oxc-parser/binding-linux-arm64-gnu@0.148.0': optional: true + '@oxc-parser/binding-linux-arm64-gnu@0.150.0': + optional: true + '@oxc-parser/binding-linux-arm64-musl@0.120.0': optional: true '@oxc-parser/binding-linux-arm64-musl@0.148.0': optional: true + '@oxc-parser/binding-linux-arm64-musl@0.150.0': + optional: true + '@oxc-parser/binding-linux-ppc64-gnu@0.120.0': optional: true '@oxc-parser/binding-linux-ppc64-gnu@0.148.0': optional: true + '@oxc-parser/binding-linux-ppc64-gnu@0.150.0': + optional: true + '@oxc-parser/binding-linux-riscv64-gnu@0.120.0': optional: true '@oxc-parser/binding-linux-riscv64-gnu@0.148.0': optional: true + '@oxc-parser/binding-linux-riscv64-gnu@0.150.0': + optional: true + '@oxc-parser/binding-linux-riscv64-musl@0.120.0': optional: true '@oxc-parser/binding-linux-riscv64-musl@0.148.0': optional: true + '@oxc-parser/binding-linux-riscv64-musl@0.150.0': + optional: true + '@oxc-parser/binding-linux-s390x-gnu@0.120.0': optional: true '@oxc-parser/binding-linux-s390x-gnu@0.148.0': optional: true + '@oxc-parser/binding-linux-s390x-gnu@0.150.0': + optional: true + '@oxc-parser/binding-linux-x64-gnu@0.120.0': optional: true '@oxc-parser/binding-linux-x64-gnu@0.148.0': optional: true + '@oxc-parser/binding-linux-x64-gnu@0.150.0': + optional: true + '@oxc-parser/binding-linux-x64-musl@0.120.0': optional: true '@oxc-parser/binding-linux-x64-musl@0.148.0': optional: true + '@oxc-parser/binding-linux-x64-musl@0.150.0': + optional: true + '@oxc-parser/binding-openharmony-arm64@0.120.0': optional: true '@oxc-parser/binding-openharmony-arm64@0.148.0': optional: true + '@oxc-parser/binding-openharmony-arm64@0.150.0': + optional: true + '@oxc-parser/binding-wasm32-wasi@0.120.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': dependencies: '@napi-rs/wasm-runtime': 1.2.5(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) @@ -5313,22 +5544,33 @@ snapshots: '@oxc-parser/binding-win32-arm64-msvc@0.148.0': optional: true + '@oxc-parser/binding-win32-arm64-msvc@0.150.0': + optional: true + '@oxc-parser/binding-win32-ia32-msvc@0.120.0': optional: true '@oxc-parser/binding-win32-ia32-msvc@0.148.0': optional: true + '@oxc-parser/binding-win32-ia32-msvc@0.150.0': + optional: true + '@oxc-parser/binding-win32-x64-msvc@0.120.0': optional: true '@oxc-parser/binding-win32-x64-msvc@0.148.0': optional: true + '@oxc-parser/binding-win32-x64-msvc@0.150.0': + optional: true + '@oxc-project/types@0.120.0': {} '@oxc-project/types@0.148.0': {} + '@oxc-project/types@0.150.0': {} + '@oxc-project/types@0.153.0': {} '@oxc-resolver/binding-android-arm-eabi@11.24.2': @@ -5638,6 +5880,10 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.87.0': optional: true + '@playwright/test@1.64.0': + dependencies: + playwright: 1.64.0 + '@rolldown/binding-android-arm-eabi@1.2.13': optional: true @@ -7086,6 +7332,10 @@ snapshots: dependencies: reusify: 1.1.0 + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 @@ -7132,6 +7382,11 @@ snapshots: flatted@3.4.4: {} + formatly@0.7.1: + dependencies: + fd-package-json: 2.0.0 + package-manager-detector: 1.9.0 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -7180,6 +7435,10 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -7398,6 +7657,22 @@ snapshots: kleur@4.1.5: {} + knip@6.40.0: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + formatly: 0.7.1 + get-tsconfig: 4.14.3 + jiti: 2.7.0 + oxc-parser: 0.150.0 + oxc-resolver: 11.24.2 + picomatch: 4.0.7 + smol-toml: 1.9.0 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.17 + unbash: 4.0.12 + yaml: 2.9.1 + zod: 4.6.5 + launch-editor@2.14.2: dependencies: picocolors: 1.1.1 @@ -7748,6 +8023,30 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc': 0.148.0 '@oxc-parser/binding-win32-x64-msvc': 0.148.0 + oxc-parser@0.150.0: + dependencies: + '@oxc-project/types': 0.150.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.150.0 + '@oxc-parser/binding-android-arm64': 0.150.0 + '@oxc-parser/binding-darwin-arm64': 0.150.0 + '@oxc-parser/binding-darwin-x64': 0.150.0 + '@oxc-parser/binding-freebsd-x64': 0.150.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.150.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.150.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.150.0 + '@oxc-parser/binding-linux-arm64-musl': 0.150.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.150.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.150.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.150.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.150.0 + '@oxc-parser/binding-linux-x64-gnu': 0.150.0 + '@oxc-parser/binding-linux-x64-musl': 0.150.0 + '@oxc-parser/binding-openharmony-arm64': 0.150.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.150.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.150.0 + '@oxc-parser/binding-win32-x64-msvc': 0.150.0 + oxc-resolver@11.24.2: optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.24.2 @@ -7898,6 +8197,8 @@ snapshots: p-try@2.2.0: {} + package-manager-detector@1.9.0: {} + parse-ms@4.0.0: {} parse5@8.0.1: @@ -7934,6 +8235,10 @@ snapshots: playwright-core@1.64.0: {} + playwright@1.64.0: + dependencies: + playwright-core: 1.64.0 + postcss-selector-parser@7.1.6: dependencies: cssesc: 3.0.0 @@ -8060,6 +8365,8 @@ snapshots: reselect@5.3.0: {} + resolve-pkg-maps@1.0.0: {} + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -8239,6 +8546,8 @@ snapshots: smart-buffer@4.2.0: {} + smol-toml@1.9.0: {} + socks@2.8.10: dependencies: ip-address: 10.7.3 @@ -8314,6 +8623,8 @@ snapshots: dependencies: min-indent: 1.0.1 + strip-json-comments@5.0.3: {} + stubborn-fs@2.0.0: dependencies: stubborn-utils: 1.0.2 @@ -8429,6 +8740,8 @@ snapshots: uint8array-extras@1.6.0: {} + unbash@4.0.12: {} + undici-types@7.24.6: {} undici@7.30.0: {} @@ -8520,6 +8833,8 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + walk-up-path@4.0.0: {} + webidl-conversions@8.0.1: {} webpack-virtual-modules@0.6.2: {} diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx index 35053c9..4368a04 100644 --- a/src/components/site-header.tsx +++ b/src/components/site-header.tsx @@ -22,6 +22,9 @@ export function SiteHeader() { Demo + + Server + { +async function fetchPosts(): Promise { const response = await fetch( "https://jsonplaceholder.typicode.com/posts?_limit=12", ); diff --git a/src/queries/server-info.ts b/src/queries/server-info.ts new file mode 100644 index 0000000..7c3f65d --- /dev/null +++ b/src/queries/server-info.ts @@ -0,0 +1,8 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { getServerInfo } from "#/server/server-info"; + +export const serverInfoQueryOptions = queryOptions({ + queryKey: ["server-info"], + queryFn: () => getServerInfo(), +}); diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 0428655..4af26cd 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -11,6 +11,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' import { Route as DemoRouteImport } from './routes/demo' +import { Route as ServerRouteImport } from './routes/server' const IndexRoute = IndexRouteImport.update({ id: '/', @@ -22,31 +23,40 @@ const DemoRoute = DemoRouteImport.update({ path: '/demo', getParentRoute: () => rootRouteImport, } as any) +const ServerRoute = ServerRouteImport.update({ + id: '/server', + path: '/server', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/demo': typeof DemoRoute + '/server': typeof ServerRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/demo': typeof DemoRoute + '/server': typeof ServerRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/demo': typeof DemoRoute + '/server': typeof ServerRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/demo' + fullPaths: '/' | '/demo' | '/server' fileRoutesByTo: FileRoutesByTo - to: '/' | '/demo' - id: '__root__' | '/' | '/demo' + to: '/' | '/demo' | '/server' + id: '__root__' | '/' | '/demo' | '/server' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute DemoRoute: typeof DemoRoute + ServerRoute: typeof ServerRoute } declare module '@tanstack/react-router' { @@ -65,12 +75,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DemoRouteImport parentRoute: typeof rootRouteImport } + '/server': { + id: '/server' + path: '/server' + fullPath: '/server' + preLoaderRoute: typeof ServerRouteImport + parentRoute: typeof rootRouteImport + } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, DemoRoute: DemoRoute, + ServerRoute: ServerRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 940423c..8f0e1b0 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -13,6 +13,7 @@ import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; import type { ReactNode } from "react"; import { SiteHeader } from "#/components/site-header"; +import { isSpa } from "#/lib/env"; import appCss from "#/styles.css?url"; @@ -58,7 +59,7 @@ function RootComponent() { // The SPA shell is prerendered once at "/" and served for every URL, so render // location-dependent UI only after hydration (see TanStack/router#8473). - if (import.meta.env.MODE === "spa") { + if (isSpa) { return {layout}; } return layout; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 6a7e9b5..d2c4636 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -25,6 +25,7 @@ import { CardHeader, CardTitle, } from "#/components/ui/card"; +import { isSpa } from "#/lib/env"; export const Route = createFileRoute("/")({ component: Home, @@ -93,8 +94,6 @@ const stack = [ }, ]; -const isSpa = import.meta.env.MODE === "spa"; - function Home() { return (
diff --git a/src/routes/server.tsx b/src/routes/server.tsx new file mode 100644 index 0000000..1f70715 --- /dev/null +++ b/src/routes/server.tsx @@ -0,0 +1,83 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import { Server } from "lucide-react"; + +import { Button } from "#/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "#/components/ui/card"; +import { isSpa } from "#/lib/env"; +import { serverInfoQueryOptions } from "#/queries/server-info"; + +export const Route = createFileRoute("/server")({ + // The static SPA has no server to call. + loader: ({ context: { queryClient } }) => + isSpa ? undefined : queryClient.query(serverInfoQueryOptions), + component: ServerFunctionDemo, +}); + +function ServerFunctionDemo() { + return ( +
+
+

+ Server functions +

+

+ createServerFn runs code on + the server and calls it from the browser like a typed function. See{" "} + src/server/server-info.ts. +

+
+ {isSpa ? : } +
+ ); +} + +function ServerInfo() { + const { data, refetch, isFetching } = useSuspenseQuery( + serverInfoQueryOptions, + ); + + return ( + + + {data.message} + + Answered at + + + + + + + ); +} + +function SpaNotice() { + return ( + + + + Server functions need the SSR build + + This is the static SPA build, so there is no server to call. + + + + pnpm build && pnpm start + + + ); +} diff --git a/src/server/server-info.ts b/src/server/server-info.ts new file mode 100644 index 0000000..6bf02b8 --- /dev/null +++ b/src/server/server-info.ts @@ -0,0 +1,9 @@ +import { createServerFn } from "@tanstack/react-start"; + +// Runs only on the server: the client bundle gets a small RPC stub instead, so +// server-only code, env vars and secrets never reach the browser. Chain +// `.validator()` before `.handler()` to accept typed input. +export const getServerInfo = createServerFn({ method: "GET" }).handler(() => ({ + message: process.env.SERVER_GREETING ?? "Hello from the server!", + time: new Date().toISOString(), +}));