Qadam Roadmap
проектplans/archived/2026-03-28-integrated/2026-03-16-web-fsd-migration.md

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/ — no next.config.ts change needed for routing. Changing @/* from "./*" to "./src/*" requires ALL existing @/ imports to remain valid, so lib/ and components/ must also move to src/ 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.json paths

    Replace the "paths" section in apps/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 to src/components/X
    • @/lib/X → resolves to src/lib/X
    • No existing imports break.
  • Step 3: Fix globals.css import in layout

    src/app/layout.tsx has import './globals.css'. After the move this still works (relative import). No change needed — verify the file exists at src/app/globals.css.

  • Step 4: Update lint scripts in package.json

    In 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 build
    

    Expected: 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.ts and messages/ stay at the repo root of apps/web/ — next-intl v4 requires them there. Only i18n/request.ts (the server config) moves into shared/.

  • Step 1: Create src/shared/api/

    Copy src/lib/api.tssrc/shared/api/api.ts (no content changes). Copy src/lib/api-types.tssrc/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.ts

    Copy src/lib/track.ts to src/shared/lib/track.ts. No content changes.

  • Step 3: Create src/shared/i18n/request.ts

    Create apps/web/src/shared/i18n/request.ts with updated relative paths. The file moves from i18n/request.ts (1 level from root) to src/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/ and shared/config/ placeholder directories

    These are part of the spec's target structure but have no files to populate in this migration. Create .gitkeep files 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.ts plugin path

    In 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 in src/app/ to use @/shared/api

    First confirm the complete list of files that need updating:

    grep -r "@/lib/" apps/web/src --include="*.ts" --include="*.tsx" -l
    

    Files to update (replace @/lib/api-types@/shared/api and @/lib/api@/shared/api):

    FileOld importNew import
    src/app/page.tsx@/lib/api@/shared/api
    src/app/page.tsx@/lib/api-types@/shared/api
    src/app/item/[slug]/page.tsx@/lib/api@/shared/api
    src/app/item/[slug]/page.tsx@/lib/api-types@/shared/api
    src/app/seller/leads/page.tsx@/lib/api-types@/shared/api
    src/app/admin/leads/page.tsx@/lib/api-types@/shared/api
    src/app/admin/page.tsx@/lib/api-types@/shared/api
    src/app/admin/reference/subjects/page.tsx@/lib/api-types@/shared/api
    src/app/admin/reference/locations/page.tsx@/lib/api-types@/shared/api
    src/app/admin/moderation/items/page.tsx@/lib/api-types@/shared/api
    src/app/me/leads/page.tsx@/lib/api-types@/shared/api

    Also update src/components/CourseFeed.tsx and src/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 build
    

    Expected: 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. CourseCard becomes ItemCard — a pure display component for a CatalogItem. No logic changes, only relocation and renaming.

  • Step 1: Create entities/item/ slice

    Copy src/components/CourseCard.tsx to src/entities/item/ui/ItemCard.tsx. Rename the exported function from CourseCard to ItemCard. 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-types
    

    Expected: 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/ or src/components/ to src/features/. The originals stay for now and are removed in Task 7 once views/ replaces them.

Note: The spec also lists features/catalog-filter/, features/submit-lead/, and features/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.tsxsrc/features/auth/ui/LoginForm.tsx. No content changes. Copy src/app/register/RegisterForm.tsxsrc/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.tsxsrc/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 to src/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.tsxsrc/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-types
    

    Expected: 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. CourseFeed uses ItemCard (from entities/item). LeadModal and ReviewsBlock move from src/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 of src/components/CourseFeed.tsx but 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.tsxsrc/widgets/lead-modal/ui/LeadModal.tsx. No content changes needed (it uses only external packages and NEXT_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.tsxsrc/widgets/reviews-block/ui/ReviewsBlock.tsx.

    ReviewsBlock has two imports to update:

    1. @/lib/api-types@/shared/api
    2. ./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-types
    

    Expected: 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.tsx is updated to just import and render it. This makes app/ 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 of src/app/page.tsx and update imports:

    • @/components/CourseFeed@/widgets/course-feed
    • @/lib/api / @/shared/api stays @/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 of src/app/item/[slug]/page.tsx (including the generateMetadata export and the default export component) and update imports:

    • @/components/CourseFeed@/widgets/course-feed
    • @/components/LeadModal@/widgets/lead-modal
    • @/components/ReviewsBlock@/widgets/reviews-block
    • @/shared/api stays as-is
    • notFound, getTranslations and 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 and generateMetadata:

    export { default as ItemDetailPage, generateMetadata } from './ui/ItemDetailPage';
    

    Update src/app/item/[slug]/page.tsx to 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 of src/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.tsx and index.ts. Update src/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 of src/app/seller/items/page.tsx. No import changes needed (it uses NEXT_PUBLIC_API_URL directly).

    Create index file. Update src/app/seller/items/page.tsx to re-export.

    Repeat for:

    • src/views/seller-dashboard/items/new/src/app/seller/items/new/page.tsx
      • Update import: ../../ItemForm@/features/seller-item-form
    • src/views/seller-dashboard/items/edit/src/app/seller/items/[id]/edit/page.tsx
      • Update import: ../../ItemForm@/features/seller-item-form
  • Step 5: Seller dashboard views (leads, profile, staff)

    Repeat the same pattern for:

    • src/views/seller-dashboard/leads/src/app/seller/leads/page.tsx
    • src/views/seller-dashboard/profile/src/app/seller/profile/page.tsx
    • src/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 of src/app/seller/onboarding/page.tsx. Update import of SellerOnboardingForm to 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.tsx
    • src/views/admin/moderation/src/app/admin/moderation/items/page.tsx
    • src/views/admin/reference/subjects/src/app/admin/reference/subjects/page.tsx
    • src/views/admin/reference/locations/src/app/admin/reference/locations/page.tsx
    • src/views/admin/leads/src/app/admin/leads/page.tsx

    Admin pages use NEXT_PUBLIC_API_URL directly for fetch calls. They also import types. By the time you copy these files (after Task 2), the source page.tsx files will already have been updated to use @/shared/api instead 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.tsxsrc/app/me/leads/page.tsx. No import changes. Create index. Update route wrapper.

  • Step 9: Verify build

    pnpm clean
    pnpm build
    

    Expected: 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 anywhere

    grep -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 anywhere

    grep -r "@/lib/" apps/web/src --include="*.ts" --include="*.tsx"
    

    Expected: no results. If any remain, update them to @/shared/api or @/shared/lib.

  • Step 3: Delete old directories

    rm -rf apps/web/src/components
    rm -rf apps/web/src/lib
    rm -rf apps/web/i18n
    

    Note: apps/web/i18n.config.ts (at the project root) is NOT deleted — it stays there as required by next-intl v4. Only the i18n/ directory (containing the old request.ts) is removed.

  • Step 4: Final build and lint

    pnpm clean
    pnpm build
    pnpm lint
    pnpm check-types
    

    Expected: 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.