7.5 KiB
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.
// 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
pnpm typecheck
- Step 3: Commit
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:
ApiHttpError(404)->{ status: 404, errorCode: "not_found" }ApiHttpError(502)->{ status: 500, errorCode: "internal" }ApiTimeoutError->{ status: 503, headers: { "Retry-After": "30" }, errorCode: "unavailable" }- Unknown error ->
{ status: 500, errorCode: "internal" }
- Step 2: Write the implementation
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
pnpm test -- src/routes/error/map.test.ts
- Step 4: Commit
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 fromcreateRootLogger()) -
<ApiClientProvider>with a default-locale ApiClient -
<ErrorBoundary>wrapping children -
Step 2: Create locale-scoped layout
src/routes/[lang]/layout.tsx:
-
Validate
params.langusingisLanguage()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
pnpm typecheck
- Step 4: Commit
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
pnpm typecheck
- Step 3: Commit
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:
"SMOKE": {
"HEADING": "Smoke test page"
}
Add to ru/common.json:
"SMOKE": {
"HEADING": "Страница проверки"
}
- Step 2: Create smoke page
src/routes/[lang]/smoke/page.tsx:
-
Uses
useTranslation()to rendert("SMOKE.HEADING") -
Uses
useLogger()to emit an info log on mount (viauseEffect) -
Displays the current locale from the URL params
-
Step 3: Verify typecheck + lint + test
pnpm typecheck && pnpm lint && pnpm test
- Step 4: Build
pnpm build:standalone
- Step 5: Commit
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 passpnpm build:standalone— succeedssrc/routes/[lang]/smoke/page.tsxexistssrc/ui/errors/ErrorBoundary.tsxexistssrc/routes/error/map.tsexists witherrorToResponse()exportedsrc/routes/error/[code]/page.tsxexistssrc/routes/layout.tsxwraps children with LoggerProvider, ApiClientProvider, ErrorBoundarysrc/routes/[lang]/layout.tsxvalidates lang and provides I18nProvider