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:
@@ -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`
|
||||
@@ -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 } },
|
||||
});
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
@@ -1,3 +0,0 @@
|
||||
import baseConfig from "@repo/core-eslint/base";
|
||||
|
||||
export default baseConfig;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
});
|
||||
@@ -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" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
11
apps/web-tanstack/src/vite-env.d.ts
vendored
11
apps/web-tanstack/src/vite-env.d.ts
vendored
@@ -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;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tags": ["app"]
|
||||
}
|
||||
@@ -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") } },
|
||||
});
|
||||
Reference in New Issue
Block a user