refactor(apps)!: delete web-tanstack demo app

Veect retrofit (ADR-027): the product is a hosted SaaS built on
web-next; apps/web-tanstack existed only as template demo surface for
the TanStack Start framework path. This slice removes the app and all
wiring that referenced it:

- turbo.json globalEnv: WEB_TANSTACK_SENTRY_DSN,
  VITE_WEB_TANSTACK_SENTRY_DSN, VITE_GIT_COMMIT_SHA,
  SENTRY_PROJECT_WEB_TANSTACK
- .env.example: the same four DSN/release vars
- generator e2e fixtures that stripped deps from the app's package.json
- coverage diff comment + test fixtures referencing apps/web-tanstack
- app-list entries in README, CLAUDE.md (port table, Sentry projects),
  AGENTS.md (tags, binder list, per-app docs), docs/glossary.md (App)
- pnpm-lock.yaml importer + orphaned transitive deps

Framework-support code in core packages stays: core-trpc's TanStack
provider, core-shared security/tanstack middleware, and the Sentry
react/node init adapters are generic TanStack support, not app wiring.
Prose in guides/ADRs/library traces is story 08 scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 15:27:37 +02:00
parent 0db1c64d99
commit 986f8b2c26
28 changed files with 33 additions and 5727 deletions

View File

@@ -1,114 +0,0 @@
# AGENTS.md — apps/web-tanstack
TanStack Start reference application using TanStack Router with file-based routing. Demonstrates that feature packages are framework-agnostic by consuming the same features as Next.js (via `@repo/core-api`) using TanStack's architecture instead.
## Purpose
Proof that features are framework-portable. This app consumes the exact same feature packages as `apps/web-next`, but through TanStack Start's server/client architecture instead of Next.js App Router. Once `@repo/core-trpc` is scaffolded, it can use the same tRPC routers via `TanstackTrpcProvider`.
## Port: 3002
```bash
pnpm dev --filter @repo/web-tanstack # http://localhost:3002
```
When `@repo/core-trpc` is installed, this app requires a tRPC endpoint (from `apps/web-next`):
```bash
pnpm dev --filter @repo/web-next # Serves tRPC at http://localhost:3000/api/trpc
pnpm dev --filter @repo/web-tanstack # http://localhost:3002
```
## Key Files
| File | Purpose |
|---|---|
| `src/routes/__root.tsx` | Root layout — wraps all routes with `<Outlet />` (add `<TanstackTrpcProvider>` after scaffolding `@repo/core-trpc`) |
| `src/routes/index.tsx` | Home page (`/`) |
| `src/routes/blog/index.tsx` | Blog listing (`/blog`) |
| `src/routes/blog/$slug.tsx` | Dynamic blog post (`/blog/:slug`) |
| `e2e/` | Playwright end-to-end tests |
## File-Based Routing
TanStack Router uses file-based routing where file paths map directly to URL routes:
| File | URL |
|---|---|
| `src/routes/__root.tsx` | Root (all routes) |
| `src/routes/index.tsx` | `/` |
| `src/routes/blog/index.tsx` | `/blog` |
| `src/routes/blog/$slug.tsx` | `/blog/:slug` |
Naming conventions:
- `__root.tsx` — special root layout
- `index.tsx` — index route for its directory
- `$paramName.tsx` — dynamic segment
## tRPC Setup (optional)
`@repo/core-trpc` is not installed by default. After scaffolding with `pnpm turbo gen core-package trpc`:
1. Update `src/routes/__root.tsx`:
```typescript
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanstackTrpcProvider } from "@repo/core-trpc/tanstack";
export const Route = createRootRoute({
component: () => (
<TanstackTrpcProvider trpcUrl="http://localhost:3000/api/trpc">
<Outlet />
</TanstackTrpcProvider>
),
});
```
Note: `trpcUrl` must point to a running tRPC endpoint (e.g., from `apps/web-next`).
2. Fetch data in routes:
```typescript
import { createFileRoute } from "@tanstack/react-router";
import { useTRPC } from "@repo/core-trpc";
import { useQuery } from "@tanstack/react-query";
export const Route = createFileRoute("/blog/$slug")({
component: BlogPostPage,
});
function BlogPostPage() {
const { slug } = Route.useParams();
const trpc = useTRPC();
const { data, isLoading } = useQuery(trpc.blog.getBySlug.queryOptions({ slug }));
if (isLoading) return <p>Loading...</p>;
return <article>{data?.title}</article>;
}
```
## Dependencies
| Dependency | Purpose |
|---|---|
| `@repo/core-api` | AppRouter type |
| `@repo/core-trpc/tanstack` | TanStack tRPC client + provider (optional — scaffold first) |
| `@repo/core-ui` | Design system components (optional — scaffold first) |
| `@tanstack/react-router` | File-based routing |
| `@tanstack/react-query` | Data fetching + caching |
| `react` / `react-dom` | React 19 runtime |
## Test conventions
- e2e tests in `e2e/` folder: `*.spec.ts`
- Playwright config in `e2e/playwright.config.ts`
- Run: `pnpm test:e2e` (both Next.js and TanStack)
Parallel to `apps/web-next` e2e: validates that features work across frameworks.
## Cross-References
- **Feature packages:** `packages/{auth,blog,media,marketing-pages,navigation}/`
- **tRPC composition:** `packages/core-api/AGENTS.md`
- **tRPC client + provider (optional):** scaffold `@repo/core-trpc` first, then see `turbo/generators/templates/core-package/trpc/AGENTS.md.hbs`
- **UI components (optional):** scaffold with `pnpm turbo gen core-package ui`, then see `turbo/generators/templates/core-package/ui/AGENTS.md.hbs`
- **Next.js app (serves tRPC):** `apps/web-next/AGENTS.md`

View File

@@ -1,37 +0,0 @@
// apps/web-tanstack/app.config.ts
// TanStack Start / Nitro server configuration.
// Registers the core-shared security headers middleware so every response
// emits the six security headers and a per-request CSP nonce.
//
// Wire-up pattern (Nitro/H3 server hook):
// withSecurityHeaders() generates nonce + builds six headers.
// setHeader calls forward them to the response.
// req.headers["x-nonce"] is set so downstream loaders can call
// getNonce(event.node.req) from @repo/core-shared/security/tanstack.
import { defineConfig } from "@tanstack/start/config";
import { withSecurityHeaders } from "@repo/core-shared/security/tanstack";
interface H3SecurityEvent {
node: {
req: { headers: Record<string, string | string[] | undefined> };
res: { setHeader: (name: string, value: string) => void };
};
}
/**
* Nitro/H3 server hook: emits six security headers on every response and
* forwards the per-request nonce in req.headers["x-nonce"] for downstream
* access via getNonce() from @repo/core-shared/security/tanstack.
*/
export function applySecurityHeaders(event: H3SecurityEvent): void {
const { nonce, headers } = withSecurityHeaders();
for (const [k, v] of Object.entries(headers)) {
event.node.res.setHeader(k, v);
}
event.node.req.headers["x-nonce"] = nonce;
}
export default defineConfig({
server: { hooks: { request: applySecurityHeaders } },
});

View File

@@ -1,12 +0,0 @@
import { test, expect } from "@playwright/test";
test.skip(
"TanStack home renders site name + nav (pending TanStack Start runtime)",
async ({ page }) => {
// Pending: web-tanstack has no dev server yet. When the TanStack Start
// runtime is wired (future plan), update the playwright.config.ts
// webServer to start it on port 3002 and remove this skip.
await page.goto("http://localhost:3002");
await expect(page.locator("h1").first()).toBeVisible();
},
);

View File

@@ -1,3 +0,0 @@
import baseConfig from "@repo/core-eslint/base";
export default baseConfig;

View File

@@ -1,45 +0,0 @@
{
"name": "@repo/web-tanstack",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "echo 'placeholder — TanStack Start build configured in later plan'",
"dev": "echo 'placeholder'",
"lint": "eslint .",
"test": "vitest run --passWithNoTests",
"test:e2e": "playwright test",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/blog": "workspace:*",
"@repo/core-api": "workspace:*",
"@repo/core-shared": "workspace:*",
"@repo/marketing-pages": "workspace:*",
"@repo/navigation": "workspace:*",
"@sentry/node": "^10.52.0",
"@sentry/react": "^10.52.0",
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-router": "^1.120.0",
"@tanstack/start": "^1.120.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"vinxi": "0.5.3"
},
"devDependencies": {
"@playwright/test": "^1.50.0",
"@repo/core-eslint": "workspace:*",
"@repo/core-testing": "workspace:*",
"@repo/core-typescript": "workspace:*",
"@sentry/vite-plugin": "^5.2.1",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitest/coverage-v8": "^3.2.4",
"jsdom": "^25.0.0",
"vitest": "^3.0.0"
}
}

View File

@@ -1,21 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: "list",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
// No webServer: web-tanstack tests run against the shared web-next backend
// (port 3000). When TanStack Start runtime is wired in a future plan, add
// a webServer block here pointing at port 3002.
});

View File

@@ -1,50 +0,0 @@
// apps/web-tanstack/src/instrumentation-client.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
// Hoist the mock so it's active when instrumentation-client runs its
// top-level initSentryClientReact call on import.
const initSentryClientReactMock = vi.hoisted(() => vi.fn());
vi.mock("@repo/core-shared/instrumentation/sentry/init-client-react", () => ({
initSentryClientReact: initSentryClientReactMock,
}));
describe("instrumentation-client", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
it("passes nonce from csp-nonce meta tag to initSentryClientReact", async () => {
const meta = document.createElement("meta");
meta.setAttribute("name", "csp-nonce");
meta.setAttribute("content", "test-nonce-xyz");
document.head.appendChild(meta);
try {
await import("./instrumentation-client");
} finally {
document.head.removeChild(meta);
}
expect(initSentryClientReactMock).toHaveBeenCalledWith(
expect.objectContaining({ nonce: "test-nonce-xyz" }),
);
});
it("passes empty string nonce when no csp-nonce meta tag is present", async () => {
await import("./instrumentation-client");
expect(initSentryClientReactMock).toHaveBeenCalledWith(
expect.objectContaining({ nonce: "" }),
);
});
it("passes web-tanstack as the app tag", async () => {
await import("./instrumentation-client");
expect(initSentryClientReactMock).toHaveBeenCalledWith(
expect.objectContaining({ app: "web-tanstack" }),
);
});
});

View File

@@ -1,18 +0,0 @@
// apps/web-tanstack/src/instrumentation-client.ts
// Browser-entry hook. Imported at the top of the client entry file.
import { initSentryClientReact } from "@repo/core-shared/instrumentation/sentry/init-client-react";
function getNonce(): string {
if (typeof document === "undefined") return "";
return (
document.querySelector('meta[name="csp-nonce"]')?.getAttribute("content") ??
""
);
}
initSentryClientReact({
dsn: import.meta.env["VITE_WEB_TANSTACK_SENTRY_DSN"],
app: "web-tanstack",
release: import.meta.env["VITE_GIT_COMMIT_SHA"],
nonce: getNonce(),
});

View File

@@ -1,12 +0,0 @@
// apps/web-tanstack/src/instrumentation.ts
// Server-entry hook. Imported at the top of the server entry file before any
// request handler runs. Initializes the OTel SDK here so PII scrub processors
// are active from the very first request (C1 fix — closes the startup window
// where Sentry auto-instrumentation could send unscrubbed errors).
import { initOtelServerNode } from "@repo/core-shared/instrumentation/otel/init-server-node";
initOtelServerNode({
dsn: process.env["WEB_TANSTACK_SENTRY_DSN"] ?? "",
serviceName: "web-tanstack",
environment: process.env["NODE_ENV"] ?? "development",
});

View File

@@ -1,29 +0,0 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
// Mock @tanstack/react-router so we don't need a full router context.
// useLoaderData is supplied so the component can read the nonce from loader data.
vi.mock("@tanstack/react-router", () => ({
createRootRoute: vi.fn((opts: { component: React.ComponentType }) => ({
options: { component: opts.component },
useLoaderData: () => ({ nonce: "test-nonce-abc" }),
})),
Outlet: () => <div data-testid="outlet" />,
}));
describe("Root route", () => {
it("renders csp-nonce meta tag and Outlet", async () => {
const { Route } = await import("./__root");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const RootComponent = (Route as any).options
.component as React.ComponentType;
render(<RootComponent />);
expect(screen.getByTestId("outlet")).toBeInTheDocument();
const metaTag = document.querySelector('meta[name="csp-nonce"]');
expect(metaTag).toBeInTheDocument();
expect(metaTag?.getAttribute("content")).toBe("test-nonce-abc");
});
});

View File

@@ -1,25 +0,0 @@
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { getNonce } from "@repo/core-shared/security/tanstack";
export const Route = createRootRoute({
loader: async () => {
try {
// Server-side during SSR: read nonce set by applySecurityHeaders middleware.
// Fails gracefully on client-side navigation (nonce already in DOM from SSR).
const { getEvent } = await import("vinxi/http");
return { nonce: getNonce(getEvent().node.req) };
} catch {
return { nonce: "" };
}
},
component: () => {
const { nonce } = Route.useLoaderData();
return (
<>
{/* nonce exposed to client so instrumentation-client.ts can read it */}
<meta name="csp-nonce" content={nonce} />
<Outlet />
</>
);
},
});

View File

@@ -1,14 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute("/" as any)({
component: Home,
});
function Home() {
return (
<main>
<p>This page is rendered by TanStack Router and consumes the same feature packages as the Next.js app.</p>
</main>
);
}

View File

@@ -1,11 +0,0 @@
// Minimal Vite-style env typing for the instrumentation-client entry.
// When the full TanStack Start / Vite build is wired in a later plan,
// replace this with `/// <reference types="vite/client" />`.
interface ImportMetaEnv {
readonly VITE_WEB_TANSTACK_SENTRY_DSN?: string;
readonly VITE_GIT_COMMIT_SHA?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -1,4 +0,0 @@
{
"status": "passed",
"failedTests": []
}

View File

@@ -1,13 +0,0 @@
{
"extends": "@repo/core-typescript/base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"]
},
"types": ["vitest/globals", "@testing-library/jest-dom"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@@ -1,4 +0,0 @@
{
"extends": ["//"],
"tags": ["app"]
}

View File

@@ -1,26 +0,0 @@
import path from "node:path";
import { mergeConfig } from "vitest/config";
import { jsdomVitestConfig } from "@repo/core-typescript/vitest.base.jsdom";
// Coverage excludes mirror the feature-package pattern (see
// packages/auth/vitest.config.ts): framework glue is excluded, thresholds
// stay inherited from the shared base — never lowered here.
export default mergeConfig(jsdomVitestConfig, {
esbuild: { jsx: "automatic" },
test: {
coverage: {
exclude: [
// TanStack Router route definitions — framework-invoked entry
// points, covered by Playwright e2e; not unit tests
"src/routes/**",
// Server-entry OTel/Sentry boot wiring — import-time side effects
// run once at startup (same class as feature DI bootstrap).
// instrumentation-client.ts stays counted (it has a unit test).
"src/instrumentation.ts",
// Ambient type declarations — no executable code
"src/**/*.d.ts",
],
},
},
resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
});