Web FSD Migration Implementation Plan
Обновлён 1 апр. 2026 г., 12:41 · 0 комментариев
Web FSD Migration Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Migrate apps/web from flat directory structure to Feature-Sliced Design (FSD) inside src/, preserving all existing behaviour and routes.
Architecture: Bottom-up incremental migration: infrastructure → shared → entities → features → widgets → views. App Router moves into src/app/. Each task ends with a working pnpm build. All work committed to the current branch; PR to main by the developer.
Tech Stack: Next.js 15 (App Router), React 19, TypeScript 5.6, next-intl 4.8.3, Tailwind CSS 3.4, pnpm workspaces / Turbo.
Spec: docs/superpowers/specs/2026-03-16-web-fsd-migration-design.md
Chunk 1: Infrastructure + Shared
Task 1: Infrastructure — move to src/, update aliases
Files:
- Move:
apps/web/app/→apps/web/src/app/ - Move:
apps/web/components/→apps/web/src/components/(temporary) - Move:
apps/web/lib/→apps/web/src/lib/(temporary) - Modify:
apps/web/tsconfig.json - Modify:
apps/web/package.json
Context: Next.js 15 auto-detects
src/app/— nonext.config.tschange needed for routing. Changing@/*from"./*"to"./src/*"requires ALL existing@/imports to remain valid, solib/andcomponents/must also move tosrc/in this same step.
-
Step 1: Move directories into
src/Run from
apps/web/:mkdir -p src mv app src/app mv components src/components mv lib src/lib -
Step 2: Update
tsconfig.jsonpathsReplace the
"paths"section inapps/web/tsconfig.json:{ "extends": "@repo/typescript-config/nextjs.json", "compilerOptions": { "plugins": [{ "name": "next" }], "paths": { "@/shared/*": ["./src/shared/*"], "@/entities/*": ["./src/entities/*"], "@/features/*": ["./src/features/*"], "@/widgets/*": ["./src/widgets/*"], "@/views/*": ["./src/views/*"], "@/*": ["./src/*"] }, "allowJs": true, "incremental": true }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts" ], "exclude": ["node_modules"] }After this change:
@/components/X→ resolves tosrc/components/X✓@/lib/X→ resolves tosrc/lib/X✓- No existing imports break.
-
Step 3: Fix
globals.cssimport in layoutsrc/app/layout.tsxhasimport './globals.css'. After the move this still works (relative import). No change needed — verify the file exists atsrc/app/globals.css. -
Step 4: Update lint scripts in
package.jsonIn
apps/web/package.json, change:"lint": "eslint \"app/**/*.{ts,tsx}\" \"lib/**/*.ts\"", "lint:fix": "eslint \"app/**/*.{ts,tsx}\" \"lib/**/*.ts\" --fix"to:
"lint": "eslint \"src/**/*.{ts,tsx}\"", "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix" -
Step 5: Clear cache and verify build
Run from
apps/web/:pnpm clean pnpm buildExpected: build succeeds with no TypeScript errors.
-
Step 6: Commit
git add apps/web/src apps/web/tsconfig.json apps/web/package.json git commit -m "refactor(web): move app/, components/, lib/ into src/ and update path aliases"
Task 2: Create shared/ layer
Files:
- Create:
apps/web/src/shared/api/index.ts - Create:
apps/web/src/shared/lib/track.ts - Create:
apps/web/src/shared/i18n/request.ts - Modify:
apps/web/next.config.ts - Modify: all files in
apps/web/src/app/that import from@/lib/
Context:
i18n.config.tsandmessages/stay at the repo root ofapps/web/— next-intl v4 requires them there. Onlyi18n/request.ts(the server config) moves intoshared/.
-
Step 1: Create
src/shared/api/Copy
src/lib/api.ts→src/shared/api/api.ts(no content changes). Copysrc/lib/api-types.ts→src/shared/api/api-types.ts(no content changes).Create
src/shared/api/index.ts:export { api } from './api'; export type { CatalogItem, CatalogResponse, SellerProfile, ItemDetail, ItemDetailResponse, Subject, Location, ReviewResponse, ReviewsResponse, ModerationItem, ModerationItemDetail, ModerationResult, AdminStats, AdminLead, LeadResponse, } from './api-types'; -
Step 2: Create
src/shared/lib/track.tsCopy
src/lib/track.tstosrc/shared/lib/track.ts. No content changes. -
Step 3: Create
src/shared/i18n/request.tsCreate
apps/web/src/shared/i18n/request.tswith updated relative paths. The file moves fromi18n/request.ts(1 level from root) tosrc/shared/i18n/request.ts(3 levels from root), so paths gain two additional../:import { getRequestConfig } from 'next-intl/server'; import { cookies } from 'next/headers'; import { i18nConfig } from '../../../i18n.config'; export default getRequestConfig(async () => { const cookieStore = await cookies(); const savedLocale = cookieStore.get('locale')?.value; const locale = savedLocale && (i18nConfig.locales as readonly string[]).includes(savedLocale) ? savedLocale : i18nConfig.defaultLocale; return { locale, // Webpack resolves dynamic imports with static string prefix correctly // regardless of how many ../ are in the prefix — this is the standard pattern. messages: (await import(`../../../messages/${locale}.json`)).default, }; }); -
Step 4: Create
shared/ui/andshared/config/placeholder directoriesThese are part of the spec's target structure but have no files to populate in this migration. Create
.gitkeepfiles so the directories exist in the repo:mkdir -p apps/web/src/shared/ui touch apps/web/src/shared/ui/.gitkeep mkdir -p apps/web/src/shared/config touch apps/web/src/shared/config/.gitkeep -
Step 5: Update
next.config.tsplugin pathIn
apps/web/next.config.ts, change line 5:// Before: const withNextIntl = createNextIntlPlugin('./i18n/request.ts'); // After: const withNextIntl = createNextIntlPlugin('./src/shared/i18n/request.ts'); -
Step 6: Update
@/lib/*imports insrc/app/to use@/shared/apiFirst confirm the complete list of files that need updating:
grep -r "@/lib/" apps/web/src --include="*.ts" --include="*.tsx" -lFiles to update (replace
@/lib/api-types→@/shared/apiand@/lib/api→@/shared/api):File Old import New import src/app/page.tsx@/lib/api@/shared/apisrc/app/page.tsx@/lib/api-types@/shared/apisrc/app/item/[slug]/page.tsx@/lib/api@/shared/apisrc/app/item/[slug]/page.tsx@/lib/api-types@/shared/apisrc/app/seller/leads/page.tsx@/lib/api-types@/shared/apisrc/app/admin/leads/page.tsx@/lib/api-types@/shared/apisrc/app/admin/page.tsx@/lib/api-types@/shared/apisrc/app/admin/reference/subjects/page.tsx@/lib/api-types@/shared/apisrc/app/admin/reference/locations/page.tsx@/lib/api-types@/shared/apisrc/app/admin/moderation/items/page.tsx@/lib/api-types@/shared/apisrc/app/me/leads/page.tsx@/lib/api-types@/shared/apiAlso update
src/components/CourseFeed.tsxandsrc/components/CourseCard.tsx:@/lib/api-types→@/shared/api
Example diff for
src/app/page.tsx:// Before: import { CourseFeed } from '@/components/CourseFeed'; import { api } from '@/lib/api'; import type { CatalogResponse } from '@/lib/api-types'; // After: import { CourseFeed } from '@/components/CourseFeed'; import { api, type CatalogResponse } from '@/shared/api'; -
Step 7: Clear cache and verify build
pnpm clean pnpm buildExpected: build succeeds.
-
Step 8: Commit
git add apps/web/src/shared apps/web/next.config.ts apps/web/src/app apps/web/src/components git commit -m "refactor(web): add shared/ layer — migrate lib/ and i18n/ into src/shared/"
Chunk 2: Entities + Features + Widgets
Task 3: Create entities/ layer
Files:
- Create:
apps/web/src/entities/item/ui/ItemCard.tsx - Create:
apps/web/src/entities/item/index.ts - Create:
apps/web/src/entities/seller/index.ts - Create:
apps/web/src/entities/lead/index.ts - Create:
apps/web/src/entities/review/index.ts - Create:
apps/web/src/entities/subject/index.ts
Context: Entities hold domain types and basic display components.
CourseCardbecomesItemCard— a pure display component for aCatalogItem. No logic changes, only relocation and renaming.
-
Step 1: Create
entities/item/sliceCopy
src/components/CourseCard.tsxtosrc/entities/item/ui/ItemCard.tsx. Rename the exported function fromCourseCardtoItemCard. Keep all logic identical.Update the import inside the new file:
// Before: import type { CatalogItem } from '@/lib/api-types'; // After: import type { CatalogItem } from '@/shared/api';Create
src/entities/item/index.ts:export { ItemCard } from './ui/ItemCard'; export type { CatalogItem, ItemDetail, ItemDetailResponse, CatalogResponse, } from '@/shared/api'; -
Step 2: Create remaining entity index files
Create
src/entities/seller/index.ts:export type { SellerProfile } from '@/shared/api';Create
src/entities/lead/index.ts:export type { LeadResponse, AdminLead } from '@/shared/api';Create
src/entities/review/index.ts:export type { ReviewResponse, ReviewsResponse } from '@/shared/api';Create
src/entities/subject/index.ts:export type { Subject, Location } from '@/shared/api'; -
Step 3: Verify types check
pnpm check-typesExpected: no errors.
-
Step 4: Commit
git add apps/web/src/entities git commit -m "refactor(web): add entities/ layer — item, seller, lead, review, subject"
Task 4: Create features/ layer
Files:
- Create:
apps/web/src/features/auth/ui/LoginForm.tsx - Create:
apps/web/src/features/auth/ui/RegisterForm.tsx - Create:
apps/web/src/features/auth/index.ts - Create:
apps/web/src/features/seller-item-form/ui/ItemForm.tsx - Create:
apps/web/src/features/seller-item-form/index.ts - Create:
apps/web/src/features/seller-onboarding/ui/SellerOnboardingForm.tsx - Create:
apps/web/src/features/seller-onboarding/index.ts
Context: Features are co-located client components that represent a user action. We copy them from their current location in
src/app/orsrc/components/tosrc/features/. The originals stay for now and are removed in Task 7 onceviews/replaces them.Note: The spec also lists
features/catalog-filter/,features/submit-lead/, andfeatures/admin-moderation/— but those are currently embedded inside page components with no standalone files to migrate. They are intentionally deferred to a future extraction pass and are NOT created in this migration.
-
Step 1: Create
features/auth/Copy
src/app/login/LoginForm.tsx→src/features/auth/ui/LoginForm.tsx. No content changes. Copysrc/app/register/RegisterForm.tsx→src/features/auth/ui/RegisterForm.tsx. No content changes.Create
src/features/auth/index.ts:export { LoginForm } from './ui/LoginForm'; export { RegisterForm } from './ui/RegisterForm'; -
Step 2: Create
features/seller-item-form/Copy
src/app/seller/items/ItemForm.tsx→src/features/seller-item-form/ui/ItemForm.tsx. No content changes.Create
src/features/seller-item-form/index.ts:export { ItemForm } from './ui/ItemForm'; -
Step 3: Create
features/seller-onboarding/Read
src/app/seller/onboarding/to find the form component file, then copy it tosrc/features/seller-onboarding/ui/SellerOnboardingForm.tsx. No content changes.Create
src/features/seller-onboarding/index.ts:export { SellerOnboardingForm } from './ui/SellerOnboardingForm'; -
Step 4: Create
features/submit-review/Copy
src/components/ReviewForm.tsx→src/features/submit-review/ui/ReviewForm.tsx. No content changes.Create
src/features/submit-review/index.ts:export { ReviewForm } from './ui/ReviewForm'; -
Step 5: Verify types check
pnpm check-typesExpected: no errors.
-
Step 6: Commit
git add apps/web/src/features git commit -m "refactor(web): add features/ layer — auth, seller-item-form, seller-onboarding, submit-review"
Task 5: Create widgets/ layer
Files:
- Create:
apps/web/src/widgets/course-feed/ui/CourseFeed.tsx - Create:
apps/web/src/widgets/course-feed/index.ts - Create:
apps/web/src/widgets/lead-modal/ui/LeadModal.tsx - Create:
apps/web/src/widgets/lead-modal/index.ts - Create:
apps/web/src/widgets/reviews-block/ui/ReviewsBlock.tsx - Create:
apps/web/src/widgets/reviews-block/index.ts
Context: Widgets are composed UI blocks that combine entities and features.
CourseFeedusesItemCard(fromentities/item).LeadModalandReviewsBlockmove fromsrc/components/unchanged. Internal imports update to use FSD aliases.
-
Step 1: Create
widgets/course-feed/Create
src/widgets/course-feed/ui/CourseFeed.tsx. Copy content ofsrc/components/CourseFeed.tsxbut update the import:// Before: import { CourseCard } from '@/components/CourseCard'; // After: import { ItemCard } from '@/entities/item'; // Also update usage: <CourseCard ... /> → <ItemCard ... />Import for type stays the same (via
@/entities/item):import { ItemCard, type CatalogItem } from '@/entities/item';Full updated file:
import { ItemCard, type CatalogItem } from '@/entities/item'; export function CourseFeed({ items, emptyMessage, }: { items: CatalogItem[]; emptyMessage: string; }) { if (items.length === 0) { return ( <div className="flex flex-col items-center justify-center py-16 text-center"> <svg className="mb-4 h-12 w-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" /> </svg> <p className="text-sm text-gray-500">{emptyMessage}</p> </div> ); } return ( <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> {items.map((item) => ( <ItemCard key={item.id} item={item} /> ))} </div> ); }Create
src/widgets/course-feed/index.ts:export { CourseFeed } from './ui/CourseFeed'; -
Step 2: Create
widgets/lead-modal/Copy
src/components/LeadModal.tsx→src/widgets/lead-modal/ui/LeadModal.tsx. No content changes needed (it uses only external packages andNEXT_PUBLIC_API_URL).Create
src/widgets/lead-modal/index.ts:export { LeadModal } from './ui/LeadModal'; -
Step 3: Create
widgets/reviews-block/Copy
src/components/ReviewsBlock.tsx→src/widgets/reviews-block/ui/ReviewsBlock.tsx.ReviewsBlockhas two imports to update:@/lib/api-types→@/shared/api./ReviewForm→@/features/submit-review(the sibling import must become an FSD alias import)
Updated import block in
src/widgets/reviews-block/ui/ReviewsBlock.tsx:import type { ReviewsResponse } from '@/shared/api'; import { ReviewForm } from '@/features/submit-review';Create
src/widgets/reviews-block/index.ts:export { ReviewsBlock } from './ui/ReviewsBlock'; -
Step 4: Verify types check
pnpm check-typesExpected: no errors.
-
Step 5: Commit
git add apps/web/src/widgets git commit -m "refactor(web): add widgets/ layer — course-feed, lead-modal, reviews-block"
Chunk 3: Views + Cleanup
Task 6: Create views/ layer
Files: One view component per route (17 views total). Each view is the page body moved out of src/app/*/page.tsx. The page.tsx files become thin wrappers.
Context: A view is a server or client component containing the full page logic. The corresponding
src/app/*/page.tsxis updated to just import and render it. This makesapp/a pure routing layer.Pattern for a server component page:
// src/app/page.tsx (after) import { CatalogPage } from '@/views/catalog'; export default CatalogPage; // (or: export { CatalogPage as default } from '@/views/catalog')Pattern for a client component page (those with
'use client'at the top):// src/app/admin/page.tsx (after) import { AdminDashboardPage } from '@/views/admin/dashboard'; export default AdminDashboardPage;
-
Step 1: Catalog view
Create
src/views/catalog/ui/CatalogPage.tsx— copy full content ofsrc/app/page.tsxand update imports:@/components/CourseFeed→@/widgets/course-feed@/lib/api/@/shared/apistays@/shared/api
Create
src/views/catalog/index.ts:export { CatalogPage } from './ui/CatalogPage';Update
src/app/page.tsx:export { CatalogPage as default } from '@/views/catalog'; -
Step 2: Item detail view
Create
src/views/item-detail/ui/ItemDetailPage.tsx— copy full content ofsrc/app/item/[slug]/page.tsx(including thegenerateMetadataexport and the default export component) and update imports:@/components/CourseFeed→@/widgets/course-feed@/components/LeadModal→@/widgets/lead-modal@/components/ReviewsBlock→@/widgets/reviews-block@/shared/apistays as-isnotFound,getTranslationsand other Next.js imports stay as-is (they work from any server component)
Create
src/views/item-detail/index.ts— re-export both the page component andgenerateMetadata:export { default as ItemDetailPage, generateMetadata } from './ui/ItemDetailPage';Update
src/app/item/[slug]/page.tsxto be a thin wrapper:export { generateMetadata } from '@/views/item-detail'; export { ItemDetailPage as default } from '@/views/item-detail'; -
Step 3: Auth views
Create
src/views/auth/login/ui/LoginPage.tsx— copy content ofsrc/app/login/page.tsx, update import:./LoginForm→@/features/auth
Create
src/views/auth/login/index.ts:export { LoginPage } from './ui/LoginPage';Update
src/app/login/page.tsx:export { LoginPage as default } from '@/views/auth/login';Repeat for register: create
src/views/auth/register/ui/RegisterPage.tsxandindex.ts. Updatesrc/app/register/page.tsx:export { RegisterPage as default } from '@/views/auth/register'; -
Step 4: Seller dashboard views (items)
Create
src/views/seller-dashboard/items/list/ui/SellerItemsPage.tsx— copy content ofsrc/app/seller/items/page.tsx. No import changes needed (it usesNEXT_PUBLIC_API_URLdirectly).Create index file. Update
src/app/seller/items/page.tsxto re-export.Repeat for:
src/views/seller-dashboard/items/new/←src/app/seller/items/new/page.tsx- Update import:
../../ItemForm→@/features/seller-item-form
- Update import:
src/views/seller-dashboard/items/edit/←src/app/seller/items/[id]/edit/page.tsx- Update import:
../../ItemForm→@/features/seller-item-form
- Update import:
-
Step 5: Seller dashboard views (leads, profile, staff)
Repeat the same pattern for:
src/views/seller-dashboard/leads/←src/app/seller/leads/page.tsxsrc/views/seller-dashboard/profile/←src/app/seller/profile/page.tsxsrc/views/seller-dashboard/staff/←src/app/seller/staff/page.tsx
-
Step 6: Seller onboarding view
Create
src/views/seller-onboarding/ui/SellerOnboardingPage.tsx— copy content ofsrc/app/seller/onboarding/page.tsx. Update import ofSellerOnboardingFormto use@/features/seller-onboarding.Create index. Update route wrapper.
-
Step 7: Admin views
Repeat for all admin routes:
src/views/admin/dashboard/←src/app/admin/page.tsxsrc/views/admin/moderation/←src/app/admin/moderation/items/page.tsxsrc/views/admin/reference/subjects/←src/app/admin/reference/subjects/page.tsxsrc/views/admin/reference/locations/←src/app/admin/reference/locations/page.tsxsrc/views/admin/leads/←src/app/admin/leads/page.tsx
Admin pages use
NEXT_PUBLIC_API_URLdirectly for fetch calls. They also import types. By the time you copy these files (after Task 2), the sourcepage.tsxfiles will already have been updated to use@/shared/apiinstead of@/lib/api-types. Copy the already-updated versions — verify there are no@/lib/imports remaining in the copied files. -
Step 8: Me view
Create
src/views/me/leads/ui/MyLeadsPage.tsx←src/app/me/leads/page.tsx. No import changes. Create index. Update route wrapper. -
Step 9: Verify build
pnpm clean pnpm buildExpected: build succeeds with all 17 routes.
-
Step 10: Commit
git add apps/web/src/views apps/web/src/app git commit -m "refactor(web): add views/ layer — extract page components from app/ routes"
Task 7: Cleanup
Files:
-
Delete:
apps/web/src/components/(now empty — all components migrated) -
Delete:
apps/web/src/lib/(now empty — all files migrated to shared/) -
Delete:
apps/web/i18n/(moved to src/shared/i18n/) -
Step 1: Verify
src/components/is no longer imported anywheregrep -r "@/components" apps/web/src --include="*.ts" --include="*.tsx"Expected: no results. If any remain, update them to use the appropriate FSD layer.
-
Step 2: Verify
src/lib/is no longer imported anywheregrep -r "@/lib/" apps/web/src --include="*.ts" --include="*.tsx"Expected: no results. If any remain, update them to
@/shared/apior@/shared/lib. -
Step 3: Delete old directories
rm -rf apps/web/src/components rm -rf apps/web/src/lib rm -rf apps/web/i18nNote:
apps/web/i18n.config.ts(at the project root) is NOT deleted — it stays there as required by next-intl v4. Only thei18n/directory (containing the oldrequest.ts) is removed. -
Step 4: Final build and lint
pnpm clean pnpm build pnpm lint pnpm check-typesExpected: all pass with no errors.
-
Step 5: Commit
git add -A git commit -m "refactor(web): cleanup — remove migrated src/components/, src/lib/, i18n/ directories"
Summary
After all tasks the structure is:
apps/web/src/
├── app/ # Next.js routing — thin wrappers only
├── views/ # Page components (17 routes)
├── widgets/ # course-feed, lead-modal, reviews-block
├── features/ # auth, seller-item-form, seller-onboarding
├── entities/ # item, seller, lead, review, subject
└── shared/ # api, i18n, lib
All routes unchanged. All behaviour unchanged. FSD import rules enforced via directory structure and aliased imports.