Add 1F-layout implementation plan for root layout, error routes, smoke route
This commit is contained in:
@@ -0,0 +1,262 @@
|
|||||||
|
# Phase 1F-layout — Root layout + routes + error mapper contracts
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Ship the root layout provider stack, locale-scoped layout, error boundary, error-to-HTTP mapper, error pages, and smoke route — so that all downstream feature routes render inside a fully-wired provider tree (`LoggerProvider` > `ApiClientProvider` > `I18nProvider` > `ErrorBoundary`) and error handling works end-to-end.
|
||||||
|
|
||||||
|
**Architecture:** Modern.js file-based routing. `src/routes/layout.tsx` wraps children with the global providers (Logger, ApiClient, ErrorBoundary). `src/routes/[lang]/layout.tsx` validates the `lang` param, creates a request-scoped i18n instance, and wraps children with `<I18nProvider>`. Error pages live at `src/routes/error/[code]/page.tsx`. The smoke route at `src/routes/[lang]/smoke/page.tsx` exercises logger, i18n, and locale display.
|
||||||
|
|
||||||
|
**Tech Stack:** React 18, Modern.js SSR, i18next, Vitest.
|
||||||
|
|
||||||
|
**Prerequisites:** 1A-1 (skeleton), 1A-2 (MF builds), 1C (i18n), 1D (API client), 1G-logger (logger).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
| File | Responsibility | Task |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/ui/errors/ErrorBoundary.tsx` | React error boundary with retry | 1 |
|
||||||
|
| `src/routes/error/map.ts` | `errorToResponse()` mapper | 2 |
|
||||||
|
| `src/routes/error/map.test.ts` | TDD tests for mapper | 2 |
|
||||||
|
| `src/routes/layout.tsx` | Root layout with provider stack | 3 |
|
||||||
|
| `src/routes/[lang]/layout.tsx` | Locale-scoped layout | 3 |
|
||||||
|
| `src/routes/error/[code]/page.tsx` | Error page (404, 500, 503) | 4 |
|
||||||
|
| `src/i18n/locales/en/common.json` | Add SMOKE keys | 5 |
|
||||||
|
| `src/i18n/locales/ru/common.json` | Add SMOKE keys | 5 |
|
||||||
|
| `src/routes/[lang]/smoke/page.tsx` | Smoke route | 5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1 — ErrorBoundary component
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/ui/errors/ErrorBoundary.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the ErrorBoundary class component**
|
||||||
|
|
||||||
|
The ErrorBoundary must be a class component (React requirement for `componentDidCatch`). It catches errors in its subtree, renders a fallback UI with a "Retry" button that resets the boundary state.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/ui/errors/ErrorBoundary.tsx
|
||||||
|
import { Component } from "react";
|
||||||
|
import type { ReactNode, ErrorInfo } from "react";
|
||||||
|
|
||||||
|
interface ErrorBoundaryProps {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
|
state: ErrorBoundaryState = { hasError: false, error: null };
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: ErrorInfo): void {
|
||||||
|
console.error("[ErrorBoundary]", error, info.componentStack);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRetry = (): void => {
|
||||||
|
this.setState({ hasError: false, error: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) return this.props.fallback;
|
||||||
|
return (
|
||||||
|
<div role="alert">
|
||||||
|
<h2>Something went wrong</h2>
|
||||||
|
<p>{this.state.error?.message}</p>
|
||||||
|
<button type="button" onClick={this.handleRetry}>Retry</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify typecheck**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ui/errors/ErrorBoundary.tsx
|
||||||
|
git commit -m "Add ErrorBoundary class component with retry support"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2 — TDD errorToResponse mapper
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/routes/error/map.ts`
|
||||||
|
- Create: `src/routes/error/map.test.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing tests**
|
||||||
|
|
||||||
|
Tests cover all four mapping rules:
|
||||||
|
1. `ApiHttpError(404)` -> `{ status: 404, errorCode: "not_found" }`
|
||||||
|
2. `ApiHttpError(502)` -> `{ status: 500, errorCode: "internal" }`
|
||||||
|
3. `ApiTimeoutError` -> `{ status: 503, headers: { "Retry-After": "30" }, errorCode: "unavailable" }`
|
||||||
|
4. Unknown error -> `{ status: 500, errorCode: "internal" }`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the implementation**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface ErrorResponse {
|
||||||
|
status: 404 | 500 | 503;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
errorCode: "not_found" | "internal" | "unavailable";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorToResponse(error: unknown): ErrorResponse;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run tests — all must pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test -- src/routes/error/map.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/routes/error/map.ts src/routes/error/map.test.ts
|
||||||
|
git commit -m "Add errorToResponse mapper with TDD tests"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3 — Root layout + locale-scoped layout
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/routes/layout.tsx` (replace 1A-2 stub)
|
||||||
|
- Create: `src/routes/[lang]/layout.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update root layout**
|
||||||
|
|
||||||
|
Replace the stub with the real provider stack:
|
||||||
|
- `<LoggerProvider>` wrapping everything (logger from `createRootLogger()`)
|
||||||
|
- `<ApiClientProvider>` with a default-locale ApiClient
|
||||||
|
- `<ErrorBoundary>` wrapping children
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create locale-scoped layout**
|
||||||
|
|
||||||
|
`src/routes/[lang]/layout.tsx`:
|
||||||
|
- Validate `params.lang` using `isLanguage()` from `@/i18n/resolver`
|
||||||
|
- If invalid lang, redirect to `/ru/` (or render 404)
|
||||||
|
- Create i18n instance via `createI18nInstance({ locale: params.lang })`
|
||||||
|
- Wrap children with `<I18nProvider>`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify typecheck**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/routes/layout.tsx src/routes/\[lang\]/layout.tsx
|
||||||
|
git commit -m "Wire root layout provider stack and locale-scoped layout"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4 — Error pages
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/routes/error/[code]/page.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create error page component**
|
||||||
|
|
||||||
|
Simple text-based UI for codes 404, 500, 503. Renders heading, description, and a link back to home. No design system dependency.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify typecheck**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/routes/error/\[code\]/page.tsx
|
||||||
|
git commit -m "Add error pages for 404, 500, 503 codes"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5 — Smoke route + i18n keys
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/i18n/locales/en/common.json` (add SMOKE keys)
|
||||||
|
- Modify: `src/i18n/locales/ru/common.json` (add SMOKE keys)
|
||||||
|
- Create: `src/routes/[lang]/smoke/page.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add SMOKE i18n keys**
|
||||||
|
|
||||||
|
Add to `en/common.json`:
|
||||||
|
```json
|
||||||
|
"SMOKE": {
|
||||||
|
"HEADING": "Smoke test page"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `ru/common.json`:
|
||||||
|
```json
|
||||||
|
"SMOKE": {
|
||||||
|
"HEADING": "Страница проверки"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create smoke page**
|
||||||
|
|
||||||
|
`src/routes/[lang]/smoke/page.tsx`:
|
||||||
|
- Uses `useTranslation()` to render `t("SMOKE.HEADING")`
|
||||||
|
- Uses `useLogger()` to emit an info log on mount (via `useEffect`)
|
||||||
|
- Displays the current locale from the URL params
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify typecheck + lint + test**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm typecheck && pnpm lint && pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build:standalone
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/i18n/locales/en/common.json src/i18n/locales/ru/common.json src/routes/\[lang\]/smoke/page.tsx
|
||||||
|
git commit -m "Add smoke route exercising logger, i18n, and locale display"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exit gate
|
||||||
|
|
||||||
|
- [ ] `pnpm typecheck && pnpm lint && pnpm test` — all pass
|
||||||
|
- [ ] `pnpm build:standalone` — succeeds
|
||||||
|
- [ ] `src/routes/[lang]/smoke/page.tsx` exists
|
||||||
|
- [ ] `src/ui/errors/ErrorBoundary.tsx` exists
|
||||||
|
- [ ] `src/routes/error/map.ts` exists with `errorToResponse()` exported
|
||||||
|
- [ ] `src/routes/error/[code]/page.tsx` exists
|
||||||
|
- [ ] `src/routes/layout.tsx` wraps children with LoggerProvider, ApiClientProvider, ErrorBoundary
|
||||||
|
- [ ] `src/routes/[lang]/layout.tsx` validates lang and provides I18nProvider
|
||||||
Reference in New Issue
Block a user