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:
@@ -27,15 +27,12 @@ CMS_URL=http://localhost:3001
|
||||
# WEB_NEXT_SENTRY_DSN=
|
||||
# NEXT_PUBLIC_WEB_NEXT_SENTRY_DSN=
|
||||
# CMS_SENTRY_DSN=
|
||||
# WEB_TANSTACK_SENTRY_DSN=
|
||||
# VITE_WEB_TANSTACK_SENTRY_DSN=
|
||||
|
||||
# Source-map upload at build time (production only).
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
# SENTRY_ORG=
|
||||
# SENTRY_PROJECT_WEB_NEXT=
|
||||
# SENTRY_PROJECT_CMS=
|
||||
# SENTRY_PROJECT_WEB_TANSTACK=
|
||||
|
||||
# OTel trace sample rate (0.0 = none, 1.0 = all). 0.1 recommended in dev.
|
||||
# SENTRY_TRACES_SAMPLE_RATE=0.1
|
||||
@@ -45,7 +42,6 @@ CMS_URL=http://localhost:3001
|
||||
|
||||
# VERCEL_GIT_COMMIT_SHA=
|
||||
# NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA=
|
||||
# VITE_GIT_COMMIT_SHA=
|
||||
|
||||
# --- Optional: core-audit (only when `gen core-package audit` is scaffolded) ---
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ See `docs/guides/runbook.md` for the full workflow.
|
||||
|
||||
### Five tags
|
||||
|
||||
- **app** (4 packages) — `apps/web-next`, `apps/web-tanstack`, `apps/cms`, `apps/storybook`
|
||||
- **app** (3 packages) — `apps/web-next`, `apps/cms`, `apps/storybook`
|
||||
- **core-composition** (3 packages) — `packages/core-api`, `core-cms`, `core-trpc`
|
||||
- **core** (1–2 packages) — `packages/core-shared`; `core-ui` is optional (scaffold with `pnpm turbo gen core-package ui`)
|
||||
- **feature** (5 packages) — `packages/auth`, `blog`, `media`, `marketing-pages`, `navigation`
|
||||
@@ -359,7 +359,7 @@ Class names carry no `Payload` prefix — `ArticlesRepository`, `PagesRepository
|
||||
|
||||
### Apps call `bindAll()` per feature at boot
|
||||
|
||||
Each app (`web-next`, `web-tanstack`, `cms`) imports both binders per feature and uses a small dispatcher (`bindAll()`) that picks based on environment:
|
||||
Each app (`web-next`, `cms`) imports both binders per feature and uses a small dispatcher (`bindAll()`) that picks based on environment:
|
||||
|
||||
- `USE_DEV_SEED === "true"` → dev seed (explicit override; works in any `NODE_ENV`)
|
||||
- `NODE_ENV === "production"` → production (real Payload)
|
||||
@@ -581,4 +581,4 @@ Per-package documentation lives in each `AGENTS.md`:
|
||||
- `packages/core-ui/AGENTS.md` (optional — generated by `pnpm turbo gen core-package ui`; see `turbo/generators/templates/core-package/ui/AGENTS.md.hbs`)
|
||||
- `packages/auth/AGENTS.md`, `blog/AGENTS.md`, `media/AGENTS.md`, `marketing-pages/AGENTS.md`, `navigation/AGENTS.md`
|
||||
- `packages/core-eslint/AGENTS.md`, `core-typescript/AGENTS.md`, `core-testing/AGENTS.md`
|
||||
- `apps/cms/AGENTS.md`, `web-next/AGENTS.md`, `web-tanstack/AGENTS.md`, `storybook/AGENTS.md`
|
||||
- `apps/cms/AGENTS.md`, `web-next/AGENTS.md`, `storybook/AGENTS.md`
|
||||
|
||||
15
CLAUDE.md
15
CLAUDE.md
@@ -117,7 +117,7 @@ See `docs/guides/coverage.md` for the cookbook and ADR-020 for the full rational
|
||||
- **Spans + capture composed at DI bind time** — Use cases + controllers are wrapped at DI bind time in this order (outermost → innermost): `withSpan → withCapture → withAudit → withAnalytics → withConsent → factory(deps)`. Apply `withAudit` when the manifest declares `audits`, `withAnalytics` when it declares `analyticsEvents`, `withConsent` when it declares `requiresConsent`. `withSpan` is always outermost so an errored span's timing reflects the capture-and-rethrow. Repository methods are different — they call `this.tracer.startSpan(...)` and `this.logger.captureException(...)` inline per method because they own per-call attributes
|
||||
- **Capture at throw sites only, with double-report guard** — Repos capture infra errors inline; use cases + controllers capture via `withCapture` at bind time; `defineErrorMiddleware` never captures. Each error gets a non-enumerable `__sentryReported` flag the first time it's captured; `withCapture`, `OtelLogger`, and `RecordingLogger` all bail if the flag is set, so a bubbled error surfaces exactly once with the inner-most layer's tags (helper at `core-shared/instrumentation/reported-flag.ts`)
|
||||
- **PII handling is non-negotiable** — `sendDefaultPii: false` everywhere (CI grep gate); replay default-masks all text/inputs/media (allowlist starts empty); `setUser({ id })` only — no email/username; server-side PII scrubbing happens at the OTel processor layer (`PiiScrubSpanProcessor` + `PiiScrubLogRecordProcessor`) before any exporter sees the data (ADR-017 §7)
|
||||
- **Three apps, three Sentry projects** — `WEB_NEXT_SENTRY_DSN`, `CMS_SENTRY_DSN`, `WEB_TANSTACK_SENTRY_DSN`. Browser DSNs use `NEXT_PUBLIC_` (web-next) and `VITE_` (web-tanstack) prefixes
|
||||
- **Two apps, two Sentry projects** — `WEB_NEXT_SENTRY_DSN`, `CMS_SENTRY_DSN`. The browser DSN uses the `NEXT_PUBLIC_` prefix (web-next)
|
||||
- **Instrumentation binding is orthogonal to repo binding** — `bindAll()`'s Rule 0 (DSN → OTel+Sentry vs Noop) is independent of `USE_DEV_SEED` / `NODE_ENV`. Run `pnpm dev` with `WEB_NEXT_SENTRY_DSN` set to test the integration locally
|
||||
- **Cross-feature events go through `IEventBus` (E0)** — In-feature reactions are direct use-case calls, not bus publishes. The bus is for _crossing_ feature boundaries (e.g. `auth` → `marketing-pages` welcome email)
|
||||
- **Event contracts are public; handlers are private (E1)** — Publisher's `events/<x>.event.ts` is exported from the feature root barrel. Consumer's `events/handlers/on-<publisher>-<event>.handler.ts` is never re-exported (ESLint-enforced via `core-eslint/rules/no-handler-reexport`)
|
||||
@@ -146,10 +146,9 @@ Storybook MCP available at `http://localhost:6006/mcp` — use `list-all-documen
|
||||
|
||||
## Key Ports
|
||||
|
||||
| Service | Port |
|
||||
| -------------- | ---- |
|
||||
| Next.js | 3000 |
|
||||
| Payload CMS | 3001 |
|
||||
| TanStack Start | 3002 |
|
||||
| PostgreSQL | 5432 |
|
||||
| Storybook | 6006 |
|
||||
| Service | Port |
|
||||
| ----------- | ---- |
|
||||
| Next.js | 3000 |
|
||||
| Payload CMS | 3001 |
|
||||
| PostgreSQL | 5432 |
|
||||
| Storybook | 6006 |
|
||||
|
||||
@@ -12,7 +12,7 @@ Read [`docs/guides/runbook.md`](./docs/guides/runbook.md) — day-1 onboarding (
|
||||
|
||||
```bash
|
||||
pnpm install # Install + auto-wire husky pre-commit hooks
|
||||
pnpm dev # All dev servers (web-next:3000, cms:3001, web-tanstack:3002, storybook:6006)
|
||||
pnpm dev # All dev servers (web-next:3000, cms:3001, storybook:6006)
|
||||
pnpm test # All tests
|
||||
pnpm typecheck # TypeScript across all packages
|
||||
pnpm lint # ESLint (incl. 8 conformance/* rules)
|
||||
|
||||
@@ -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") } },
|
||||
});
|
||||
@@ -39,7 +39,7 @@ A tag for packages that compose feature exports — `core-api`, `core-cms`, `cor
|
||||
A package providing build/lint/test infrastructure — `core-eslint`, `core-typescript`, `core-testing`. May only depend on other tooling.
|
||||
|
||||
**App**:
|
||||
A runtime entry point under `apps/` — `web-next`, `web-tanstack`, `cms`, `storybook`.
|
||||
A runtime entry point under `apps/` — `web-next`, `cms`, `storybook`.
|
||||
|
||||
## Clean Architecture layers (per feature)
|
||||
|
||||
|
||||
5276
pnpm-lock.yaml
generated
5276
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -71,7 +71,7 @@ const ALLOWED_GLOBS = [
|
||||
// Tooling packages that don't generate a vitest lcov (no @vitest/coverage-v8)
|
||||
/^packages\/core-testing\//,
|
||||
/^packages\/core-eslint\//,
|
||||
// App packages (web-next, web-tanstack, cms) do not configure
|
||||
// App packages (web-next, cms) do not configure
|
||||
// @vitest/coverage-v8, so their source files never appear in the merged lcov.
|
||||
/^apps\//,
|
||||
// core-shared Sentry client init files — explicitly excluded from per-package
|
||||
|
||||
@@ -246,15 +246,12 @@ describe("computeDiffCoverage", () => {
|
||||
test("skips apps/ files (app packages don't configure @vitest/coverage-v8)", () => {
|
||||
const lcov = parseLcov(lcovText);
|
||||
const diff = new Map([
|
||||
// web-next and web-tanstack have no @vitest/coverage-v8 — never in lcov
|
||||
// web-next and cms have no @vitest/coverage-v8 — never in lcov
|
||||
["apps/web-next/instrumentation-client.ts", new Set([1, 2, 3, 4, 5, 6])],
|
||||
["apps/web-next/middleware.ts", new Set([1, 2, 3, 4, 5])],
|
||||
["apps/web-next/src/app/layout.tsx", new Set([1, 2, 3])],
|
||||
[
|
||||
"apps/web-tanstack/src/instrumentation-client.ts",
|
||||
new Set([1, 2, 3, 4, 5]),
|
||||
],
|
||||
["apps/web-tanstack/app.config.ts", new Set([1, 2, 3])],
|
||||
["apps/cms/src/instrumentation.ts", new Set([1, 2, 3, 4, 5])],
|
||||
["apps/cms/src/payload.config.ts", new Set([1, 2, 3])],
|
||||
]);
|
||||
const result = computeDiffCoverage(diff, lcov);
|
||||
assert.equal(result.status, "pass");
|
||||
|
||||
@@ -10,14 +10,10 @@
|
||||
"WEB_NEXT_SENTRY_DSN",
|
||||
"NEXT_PUBLIC_WEB_NEXT_SENTRY_DSN",
|
||||
"CMS_SENTRY_DSN",
|
||||
"WEB_TANSTACK_SENTRY_DSN",
|
||||
"VITE_WEB_TANSTACK_SENTRY_DSN",
|
||||
"VITE_GIT_COMMIT_SHA",
|
||||
"SENTRY_AUTH_TOKEN",
|
||||
"SENTRY_ORG",
|
||||
"SENTRY_PROJECT_WEB_NEXT",
|
||||
"SENTRY_PROJECT_CMS",
|
||||
"SENTRY_PROJECT_WEB_TANSTACK",
|
||||
"SENTRY_TRACES_SAMPLE_RATE",
|
||||
"SENTRY_ENVIRONMENT",
|
||||
"VERCEL_GIT_COMMIT_SHA",
|
||||
|
||||
@@ -67,7 +67,6 @@ describe("e2e: core-package trpc", () => {
|
||||
// and navigation features depend on it for their ./ui tRPC hooks.
|
||||
for (const pkgDir of [
|
||||
["apps", "web-next"],
|
||||
["apps", "web-tanstack"],
|
||||
["packages", "blog"],
|
||||
["packages", "marketing-pages"],
|
||||
["packages", "navigation"],
|
||||
|
||||
@@ -64,7 +64,6 @@ describe("e2e: core-package ui", () => {
|
||||
// Strip @repo/core-ui from app package.json files so pnpm install
|
||||
// succeeds without the package being present (simulating the post-removal state).
|
||||
stripCoreUiDep(join(tmp, "apps", "web-next", "package.json"));
|
||||
stripCoreUiDep(join(tmp, "apps", "web-tanstack", "package.json"));
|
||||
stripCoreUiDep(join(tmp, "apps", "storybook", "package.json"));
|
||||
|
||||
execSync(`cd ${tmp} && pnpm install --frozen-lockfile=false`, {
|
||||
|
||||
Reference in New Issue
Block a user