diff --git a/packages/b2c-dx-mcp/README.md b/packages/b2c-dx-mcp/README.md index 45b1d0be..7a16462a 100644 --- a/packages/b2c-dx-mcp/README.md +++ b/packages/b2c-dx-mcp/README.md @@ -71,6 +71,86 @@ Override auto-discovery by specifying toolsets explicitly: "args": ["--working-directory", "${workspaceFolder}", "--toolsets", "CARTRIDGES,MRT", "--allow-non-ga-tools"] ``` +### Prompting Tips and Examples + +AI assistants (like Cursor, Claude Desktop) automatically decide which MCP tools to use based on your prompts. To get the best results, use clear, specific prompts that describe what you want to accomplish. + +> ⚠️ **IMPORTANT**: **Explicitly mention "Use the MCP tool"** in your prompts for reliable tool usage. While AI assistants (like Cursor's Composer) can automatically select MCP tools based on context, explicit instructions ensure the assistant prioritizes MCP tools over general knowledge, especially when multiple approaches are possible. This is particularly important for getting project-specific, up-to-date information rather than generic responses. + +#### Best Practices + +1. **Always explicitly request MCP tool usage** (see warning above): Start prompts with "Use the MCP tool to..." or include "Use the MCP tool" in your request. +2. **Be specific about your goal**: Instead of "help me with Storefront Next", say "Use the MCP tool to show me how to build a product detail page with authentication" +3. **Mention the tool or domain explicitly**: Reference the framework (Storefront Next, PWA Kit), operation (deploy, discover), or domain (SCAPI, cartridges) +4. **Use natural language**: Describe what you want to achieve, not the tool name +5. **Provide context**: Mention your project type, what you're building, or what you need to learn +6. **Ask for guidelines first**: When starting a new project or learning a framework, ask for development guidelines before writing code + +#### Examples by Tool Category + +##### Storefront Next Development Guidelines + +The `storefront_next_development_guidelines` tool provides critical architecture rules and best practices. **Use this tool first** when starting new Storefront Next development or when you need architecture guidance. + +**Good prompts:** +- ✅ "I'm new to Storefront Next. Use the MCP tool to show me the critical rules I need to know." +- ✅ "I need to build a product detail page. Use the MCP tool to show me best practices for data fetching and component patterns." +- ✅ "I need to build a checkout form with authentication and validation. Use the MCP tool to show me how to handle form submissions, authentication, and internationalized error messages." +- ✅ "Use the MCP tool to show me the data fetching patterns for Storefront Next." +- ✅ "Show me all available Storefront Next development guidelines." + +**Available sections:** +- `quick-reference` - Critical rules and architecture principles (default) +- `data-fetching` - Data loading patterns with loaders +- `state-management` - Client-side state management +- `auth` - Authentication and session management +- `components` - Component patterns and best practices +- `styling` - Tailwind CSS 4, Shadcn/ui, styling guidelines +- `page-designer` - Page Designer integration +- `performance` - Performance optimization +- `testing` - Testing strategies +- `i18n` - Internationalization patterns +- `config` - Configuration management +- `extensions` - Extension development +- `pitfalls` - Common pitfalls + +##### PWA Kit Development + +**Good prompts:** +- ✅ "I'm starting a new PWA Kit project. Use the MCP tool to get the development guidelines." +- ✅ "Use the MCP tool to create a new product listing page component in my PWA Kit project." +- ✅ "Use the MCP tool to recommend React hooks for fetching product data in PWA Kit." +- ✅ "Use the MCP tool to explore the SCAPI Shop API endpoints available for my PWA Kit storefront." + +##### SCAPI Discovery + +**Good prompts:** +- ✅ "Use the MCP tool to discover what SCAPI endpoints are available for product data." +- ✅ "Use the MCP tool to discover custom SCAPI APIs in my B2C instance." +- ✅ "Use the MCP tool to show me all available SCAPI endpoints and their capabilities." +- ✅ "Use the MCP tool to scaffold a new custom SCAPI API for order management." + +##### Cartridge Deployment + +**Good prompts:** +- ✅ "Use the MCP tool to deploy my cartridges to the sandbox instance." +- ✅ "Use the MCP tool to deploy only the app_storefront_base cartridge to production." +- ✅ "Use the MCP tool to deploy cartridges from the ./cartridges directory and reload the code version." + +##### MRT Bundle Operations + +**Good prompts:** +- ✅ "Use the MCP tool to build and push my Storefront Next bundle to staging." +- ✅ "Use the MCP tool to push the bundle from ./build directory to Managed Runtime." +- ✅ "Use the MCP tool to deploy my PWA Kit bundle to production with a deployment message." + +#### Tips for Better Results + +- **Start with guidelines**: When learning a new framework, ask for development guidelines first using "Use the MCP tool to get..." +- **Combine related topics**: Ask for multiple related sections (e.g., "data fetching and components") in one request +- **Provide project context**: Mention your project type (Storefront Next, PWA Kit, cartridges) for better tool selection +- **Specify operations clearly**: For deployment operations, mention the target (sandbox, staging, production) and what to deploy + ### Configuration Credentials can be provided via **config files** (recommended), **environment variables**, or **flags**. Priority: Flags > Env vars > Config files. @@ -205,13 +285,13 @@ Storefront Next development tools for building modern storefronts. | Tool | Description | |------|-------------| -| `sfnext_development_guidelines` | Get Storefront Next development guidelines and best practices | -| `sfnext_site_theming` | Configure and manage site theming for Storefront Next | -| `sfnext_figma_to_component_workflow` | Convert Figma designs to Storefront Next components | -| `sfnext_generate_component` | Generate a new Storefront Next component | -| `sfnext_map_tokens_to_theme` | Map design tokens to Storefront Next theme configuration | -| `sfnext_design_decorator` | Apply design decorators to Storefront Next components | -| `sfnext_generate_page_designer_metadata` | Generate Page Designer metadata for Storefront Next components | +| `storefront_next_development_guidelines` | Get Storefront Next development guidelines and best practices | +| `storefront_next_site_theming` | Configure and manage site theming for Storefront Next | +| `storefront_next_figma_to_component_workflow` | Convert Figma designs to Storefront Next components | +| `storefront_next_generate_component` | Generate a new Storefront Next component | +| `storefront_next_map_tokens_to_theme` | Map design tokens to Storefront Next theme configuration | +| `storefront_next_design_decorator` | Apply design decorators to Storefront Next components | +| `storefront_next_generate_page_designer_metadata` | Generate Page Designer metadata for Storefront Next components | | `scapi_discovery` | Discover available SCAPI endpoints and capabilities | | `scapi_custom_api_discovery` | Discover custom SCAPI API endpoints | | `mrt_bundle_push` | Build, push bundle (optionally deploy) | @@ -287,7 +367,7 @@ npx mcp-inspector --cli node bin/dev.js --toolsets all --allow-non-ga-tools --me # Call a specific tool npx mcp-inspector --cli node bin/dev.js --toolsets all --allow-non-ga-tools \ --method tools/call \ - --tool-name sfnext_design_decorator + --tool-name storefront_next_design_decorator ``` #### 2. IDE Integration diff --git a/packages/b2c-dx-mcp/content/auth.md b/packages/b2c-dx-mcp/content/auth.md new file mode 100644 index 00000000..b962df2f --- /dev/null +++ b/packages/b2c-dx-mcp/content/auth.md @@ -0,0 +1,62 @@ +# Authentication & Session Management + +## Architecture + +Split-cookie architecture with server/client contexts: + +- **Server middleware** (`auth.server.ts`): Manages SLAS tokens, writes cookies +- **Client middleware** (`auth.client.ts`): Reads cookies, maintains cache +- **React Context** (`AuthProvider`): Provides auth state to components + +## Cookie Design + +| Cookie Name | Purpose | User Type | Expiry | HttpOnly | +|-------------|---------|-----------|--------|----------| +| `cc-nx-g` | Guest refresh token | Guest | 30 days | No | +| `cc-nx` | Registered refresh token | Registered | 90 days | No | +| `cc-at` | Access token | Both | 30 min | No | +| `usid` | User session ID | Both | Matches refresh | No | +| `customerId` | Customer ID | Registered | Matches refresh | No | + +**Key Points**: + +- Only ONE refresh token exists (guest OR registered, never both) +- User type derived from which refresh token exists +- Cookies auto-namespaced with `siteId` +- Tokens auto-refresh when expired + +## Usage in Loaders/Actions + +```typescript +import { getAuth } from '@/middlewares/auth.server'; + +export function loader({ context }: LoaderFunctionArgs) { + const auth = getAuth(context); + + // Access auth properties + const accessToken = auth.access_token; + const customerId = auth.customer_id; + const isGuest = auth.userType === 'guest'; + const isRegistered = auth.userType === 'registered'; + + return { isGuest, customerId }; +} +``` + +## Usage in Components + +```typescript +import { useAuth } from '@/providers/auth'; + +export function MyComponent() { + const auth = useAuth(); + + if (auth?.userType === 'guest') { + return ; + } + + return
Welcome, customer {auth?.customer_id}
; +} +``` + +**Reference:** See README-AUTH.md for complete authentication documentation. diff --git a/packages/b2c-dx-mcp/content/components.md b/packages/b2c-dx-mcp/content/components.md new file mode 100644 index 00000000..d2dfe1a5 --- /dev/null +++ b/packages/b2c-dx-mcp/content/components.md @@ -0,0 +1,123 @@ +# Component Patterns + +## Use the `createPage` HOC + +The `createPage` higher-order component standardizes page patterns with built-in Suspense and page key handling: + +```typescript +import { use } from 'react'; +import { createPage } from '@/components/create-page'; + +// Define your view component +function ProductView({ + product, + category +}: { + product: Promise; + category?: Promise +}) { + const productData = use(product); + const categoryData = category ? use(category) : null; + + return ( +
+

{productData.name}

+ {categoryData &&

Category: {categoryData.name}

} +
+ ); +} + +// Create page with fallback +const ProductPage = createPage({ + component: ProductView, + fallback: +}); + +export default ProductPage; +``` + +**Benefits:** + +- Eliminates repetitive Suspense/Await boilerplate +- Consistent loading states across pages +- Built-in page key management for navigation transitions +- Type-safe with full TypeScript support + +## shadcn/ui Components + +**RULES**: + +- ✅ Add via: `npx shadcn@latest add ` +- ❌ DO NOT modify `src/components/ui/` directly +- ✅ Create custom components elsewhere + +## Suspense Boundaries + +Use granular Suspense boundaries for better UX: + +```typescript +// ✅ RECOMMENDED - Multiple Suspense boundaries +export default function ProductPage({ loaderData: { product, reviews } }) { + return ( +
+ }> + + {(data) => } + + + + }> + + {(data) => } + + +
+ ); +} + +// ⚠️ OK - Single Suspense boundary (less granular) +export default createPage({ + component: ProductView, + fallback: +}); +``` + +## File Organization + +``` +src/components/product-tile/ +├── index.tsx # Component +├── index.test.tsx # Tests +└── stories/ + ├── index.stories.tsx # Storybook stories + └── __snapshots__/ # Storybook snapshots (optional) + └── product-tile-snapshot.tsx.snap + +# Skeleton components are separate components +src/components/product-skeleton/ +├── index.tsx +├── index.test.tsx +└── stories/ + └── index.stories.tsx +``` + +## Styling + +**Tailwind CSS 4** is the only styling approach allowed. Use utility classes directly in components. + +**Key rules:** + +- ✅ Use Tailwind utility classes +- ✅ Use `cn()` utility for conditional classes +- ❌ NO inline styles, NO CSS modules, NO separate CSS files + +**See `styling` section for:** Tailwind CSS 4, Shadcn/ui components, icons, responsive design, theme configuration, dark mode, best practices + +## Best Practices + +1. **Extract view components** - Separate data handling from presentation +2. **Type safety** - Define proper TypeScript interfaces +3. **Consistent fallbacks** - Reusable skeleton components +4. **Colocate tests** - Keep tests next to components +5. **Story coverage** - Create stories for all reusable components +6. **Tailwind utilities only** - Use Tailwind CSS classes, avoid inline styles or CSS modules diff --git a/packages/b2c-dx-mcp/content/config.md b/packages/b2c-dx-mcp/content/config.md new file mode 100644 index 00000000..5b45328f --- /dev/null +++ b/packages/b2c-dx-mcp/content/config.md @@ -0,0 +1,180 @@ +# Configuration Management + +## Overview + +All configuration is centralized in `config.server.ts` with environment variable overrides via `.env` files. The configuration system provides type-safe access to app settings with automatic parsing and validation. + +## Required Variables + +Copy `.env.default` to `.env` and set these required Commerce Cloud credentials: + +```bash +PUBLIC__app__commerce__api__clientId=your-client-id +PUBLIC__app__commerce__api__organizationId=your-org-id +PUBLIC__app__commerce__api__siteId=your-site-id +PUBLIC__app__commerce__api__shortCode=your-short-code +PUBLIC__app__defaultSiteId=your-site-id +PUBLIC__app__commerce__sites='[{"id":"your-site-id","defaultLocale":"en-US","defaultCurrency":"USD","supportedLocales":[{"id":"en-US","preferredCurrency":"USD"}],"supportedCurrencies":["USD"]}]' +``` + +**Note:** The `commerce.sites` array defines your site configuration including locales, currencies, and supported options. See `.env.default` for a complete example with multiple locales and currencies. + +## Adding Configuration + +1. **Define type in `src/config/schema.ts`**: + +```typescript +export type Config = { + app: { + myFeature: { + enabled: boolean; + maxItems: number; + }; + }; +}; +``` + +2. **Add defaults in `config.server.ts`**: + +```typescript +export default defineConfig({ + app: { + myFeature: { + enabled: false, + maxItems: 10, + }, + }, +}); +``` + +3. **Override via environment variables**: + +```bash +PUBLIC__app__myFeature__enabled=true +PUBLIC__app__myFeature__maxItems=20 +``` + +## Usage Patterns + +**In React Components**: + +```typescript +import { useConfig } from '@/config'; + +export function MyComponent() { + const config = useConfig(); + + if (config.myFeature.enabled) { + const maxItems = config.myFeature.maxItems; + // Your feature code + } +} +``` + +**In Server Loaders/Actions**: + +```typescript +import { getConfig } from '@/config'; + +export function loader({ context }: LoaderFunctionArgs) { + const config = getConfig(context); + + if (config.myFeature.enabled) { + // Your loader code + } +} +``` + +**In Client Loaders**: + +```typescript +import { getConfig } from '@/config'; + +export function clientLoader() { + const config = getConfig(); // No context needed - uses window.__APP_CONFIG__ + + if (config.myFeature.enabled) { + // Your loader code + } +} +``` + +**Note:** `getConfig()` and `useConfig()` return `AppConfig` which is the `app` section of the full `Config` type. So you access properties directly (e.g., `config.myFeature.enabled`) without the `app` prefix. + +## Environment Variable Rules + +Use the `PUBLIC__` prefix with double underscores (`__`) to set any config path: + +```bash +# Environment variable → Config path (in Config type) → Access via getConfig()/useConfig() +PUBLIC__app__commerce__sites='[...]' → config.app.commerce.sites → config.commerce.sites +PUBLIC__app__defaultSiteId=RefArchGlobal → config.app.defaultSiteId → config.defaultSiteId +PUBLIC__app__myFeature__enabled=true → config.app.myFeature.enabled → config.myFeature.enabled +``` + +**Multi-site Configuration Example:** + +```bash +PUBLIC__app__commerce__sites='[ + { + "id": "RefArchGlobal", + "defaultLocale": "en-US", + "defaultCurrency": "USD", + "supportedLocales": [ + {"id": "en-US", "preferredCurrency": "USD"}, + {"id": "de-DE", "preferredCurrency": "EUR"} + ], + "supportedCurrencies": ["USD", "EUR"] + } +]' +``` + +**Accessing Site Configuration:** + +```typescript +const config = getConfig(context); +const currentSite = config.commerce.sites[0]; // Get first site +const locale = currentSite.defaultLocale; // "en-US" +const currency = currentSite.defaultCurrency; // "USD" +``` + +Values are automatically parsed (numbers, booleans, JSON arrays/objects). + +Rules: +1. **`PUBLIC__` prefix**: Exposed to browser (client-safe values) +2. **No prefix**: Server-only (secrets, never exposed) +3. **`__` separator**: Navigate nested paths (`PUBLIC__app__commerce__sites`) +4. **Case-insensitive**: All casings work (normalized to match `config.server.ts`) +5. **Auto-parsing**: Strings, numbers, booleans, JSON arrays/objects +6. **Validation**: Paths must exist in `config.server.ts` (prevents typos) +7. **Depth limit**: Maximum 10 levels deep (use JSON values for deeper nesting) +8. **Path precedence**: More specific paths override less specific ones +9. **Protected paths**: `app__engagement` cannot be overridden via environment variables +10. **MRT limits**: Variable names max 512 characters, total PUBLIC__ values max 32KB + +**Note:** Site configuration (locales, currencies) is now managed via `PUBLIC__app__commerce__sites` array instead of individual `PUBLIC__app__site__locale` variables. This enables multi-site support. + +**Setting nested objects with JSON:** + +```bash +# Instead of multiple variables: +PUBLIC__app__myFeature__option1=value1 +PUBLIC__app__myFeature__option2=value2 + +# Use a single JSON value: +PUBLIC__app__myFeature='{"option1":"value1","option2":"value2","nested":{"enabled":true}}' +``` + +## Security + +```bash +# ✅ Safe for client (PUBLIC__ prefix) +PUBLIC__app__commerce__api__clientId=abc123 + +# ✅ Server-only (no prefix) +COMMERCE_API_SLAS_SECRET=your-secret +``` + +Read server-only secrets directly from `process.env` - never add to config. + +**Reference:** See src/config/README.md for complete configuration documentation. diff --git a/packages/b2c-dx-mcp/content/data-fetching.md b/packages/b2c-dx-mcp/content/data-fetching.md new file mode 100644 index 00000000..111e5889 --- /dev/null +++ b/packages/b2c-dx-mcp/content/data-fetching.md @@ -0,0 +1,323 @@ +# Data Fetching Patterns + +## Loader Functions + +**IMPORTANT**: This project **mandates server-only data loading**. Every UI route must only export a `loader` function. + +### Critical Rule: Synchronous Loaders for Streaming + +**IMPORTANT**: Loaders should be **synchronous functions that return objects containing promises**, NOT async functions. This enables non-blocking page transitions and streaming SSR. + +```typescript +// ✅ CORRECT - Synchronous loader returning promises +export function loader({ context }: LoaderFunctionArgs): ProductPageData { + const clients = createApiClients(context); + return { + product: clients.shopperProducts.getProduct({...}), // Promise - streams + reviews: clients.shopperProducts.getReviews({...}), // Promise - streams + }; +} + +// ❌ AVOID - Async loader blocks page transitions +export async function loader({ context }: LoaderFunctionArgs): Promise { + const product = await clients.shopperProducts.getProduct({...}); // Blocks! + return { product }; +} +``` + +**Why this matters:** +- Async loaders with `await` **block the entire page transition** until all data resolves +- Synchronous loaders returning promises allow React to **stream data progressively** +- Each promise resolves independently, enabling granular Suspense boundaries +- Users see content as it becomes available, not all at once + +**Behavior**: +- Initial load: Runs on server (SSR) +- Navigation: Runs on server (XHR/fetch to server) +- SCAPI requests always on MRT + +## Data Loading Strategies + +### Pattern 1: Awaited Data (Blocking) + +```typescript +// ⚠️ BLOCKS rendering until all data is ready +export async function loader({ params, context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + return { + product: await clients.shopperProducts.getProduct({ + params: { path: { id: params.productId } } + }).then(({ data }) => data) + }; +} +``` + +**Use when:** Critical data must be available before rendering (SEO, above-the-fold content) + +### Pattern 2: Deferred Data (Streaming) + +```typescript +// ✅ RECOMMENDED - Streams data progressively +export function loader({ params, context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + return { + // Return promises directly - they'll stream to client + product: clients.shopperProducts.getProduct({ + params: { path: { id: params.productId } } + }).then(({ data }) => data), + + reviews: clients.shopperProducts.getReviews({ + params: { path: { id: params.productId } } + }).then(({ data }) => data) + }; +} +``` + +**Use when:** Non-critical data can load after initial render + +### Pattern 3: Mixed Strategy + +```typescript +// ✅ BEST OF BOTH - Critical data awaited, rest streamed +export async function loader({ params, context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + + // Await critical data + const product = await clients.shopperProducts.getProduct({ + params: { path: { id: params.productId } } + }).then(({ data }) => data); + + return { + product, // Resolved + reviews: clients.shopperProducts.getReviews({ + params: { path: { id: params.productId } } + }).then(({ data }) => data), // Streamed + recommendations: clients.shopperProducts.getRecommendations({ + params: { path: { id: params.productId } } + }).then(({ data }) => data) // Streamed + }; +} +``` + +## Action Functions + +Handle mutations (form submissions, cart updates): + +```typescript +import {data, redirect} from 'react-router'; + +export async function action({request, context}: ActionFunctionArgs) { + const formData = await request.formData(); + const productId = formData.get('productId') as string; + + const clients = createApiClients(context); + + try { + await clients.shopperBasketsV2.addItemToBasket({ + params: { + path: {basketId}, + body: {productId, quantity: 1}, + }, + }); + + return data({success: true}); + } catch (error) { + return data({success: false, error: error.message}, {status: 400}); + } +} +``` + +## Interactive Data Fetching: useScapiFetcher + +For on-demand, user-triggered data fetching (after page load), use the `useScapiFetcher` hook instead of loaders. + +### `loader` vs `useScapiFetcher` + +| Aspect | `loader` | `useScapiFetcher` | +|--------|----------|-------------------| +| **When it runs** | Route navigation (page load) | On-demand (user interaction) | +| **Triggered by** | URL change | Component code (useEffect, button click) | +| **Data availability** | Before/during component render (streamed) | After component mounts | +| **Execution context** | Server (MRT) | Triggers server route | +| **Use case** | Initial page data | Dynamic, interactive fetching | + +### How `useScapiFetcher` Works + +```text +Component calls useScapiFetcher() + ↓ +Hook builds URL: /resource/api/client/{encoded-params} + ↓ +fetcher.load() or fetcher.submit() + ↓ +resource.api.client.$resource.ts loader/action runs ON SERVER + ↓ +createApiClients(context) makes SCAPI call (server-side) + ↓ +JSON response returned to component +``` + +**Important:** Even though you call `useScapiFetcher` from the browser, the actual SCAPI requests still happen **on the server** through the resource route, keeping credentials secure. + +### Example: Search Suggestions + +```typescript +import { useScapiFetcher } from '@/hooks/use-scapi-fetcher'; +import { useMemo, useCallback } from 'react'; + +export function useSearchSuggestions({ q, limit, currency }) { + // Prepare SCAPI parameters + const parameters = useMemo( + () => ({ + params: { + query: { q, limit, currency } + } + }), + [q, limit, currency] + ); + + // Hook automatically routes to server + const fetcher = useScapiFetcher( + 'shopperSearch', // SCAPI client + 'getSearchSuggestions', // Method name + parameters // Parameters + ); + + const refetch = useCallback(async () => { + await fetcher.load(); // Triggers server request + }, [fetcher]); + + return { + data: fetcher.data, + isLoading: fetcher.state === 'loading', + refetch + }; +} +``` + +### When to Use Each Approach + +| Scenario | Use | +|----------|-----| +| Load product data when visiting `/product/123` | `loader` | +| Load checkout data | `loader` | +| Search suggestions as user types | `useScapiFetcher` | +| Update customer profile in modal | `useScapiFetcher` | +| Load recommendations after page loads | `useScapiFetcher` | +| Fetch bonus products when modal opens | `useScapiFetcher` | +| Infinite scroll / Load more | `useScapiFetcher` | + +### Timeline Comparison + +```text +┌─────────────────────────────────────────────────────────────────┐ +│ loader (Server) │ +├─────────────────────────────────────────────────────────────────┤ +│ User clicks link → Server loader() → Stream data → Page render │ +│ │ +│ Timeline: [navigate] → [server fetch] → [stream to client] │ +│ │ +│ Data available: Streamed during render via Suspense │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ useScapiFetcher │ +├─────────────────────────────────────────────────────────────────┤ +│ Page loads → Component mounts → User types → fetcher.load() │ +│ │ +│ Timeline: [render] → [user action] → [fetch] → [re-render] │ +│ │ +│ Data available: AFTER user action, component re-renders │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## API Client Usage + +Always use `createApiClients(context)`: + +```typescript +import { createApiClients } from '@/lib/api-clients'; + +export function loader({ context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + + // All SCAPI clients with full type safety: + clients.shopperProducts.getProduct({...}); + clients.shopperCustomers.getCustomer({...}); + clients.shopperBasketsV2.getBasket({...}); + clients.shopperSearch.productSearch({...}); + clients.shopperOrders.getOrder({...}); +} +``` + +## Parallel vs Sequential + +```typescript +// ✅ GOOD - Parallel requests +export function loader({ context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + + return { + product: clients.shopperProducts.getProduct({...}), + reviews: clients.shopperProducts.getReviews({...}), + recommendations: clients.shopperProducts.getRecommendations({...}) + }; + // All three requests start simultaneously +} + +// ❌ BAD - Sequential requests +export async function loader({ context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + + const product = await clients.shopperProducts.getProduct({...}); + const reviews = await clients.shopperProducts.getReviews({...}); + const recommendations = await clients.shopperProducts.getRecommendations({...}); + + return { product, reviews, recommendations }; + // Each request waits for the previous to complete +} +``` + +## Understanding Data Flow + +### Initial Page Load (SSR) + +```text +Browser → MRT Server + ↓ + loader() runs on server + ↓ + SCAPI requests on MRT + ↓ + HTML response → Browser +``` + +**Key characteristics:** +- The `loader()` runs on the server +- SCAPI requests happen server-side (direct in production, proxied in dev) +- Full HTML is returned to browser +- Client hydrates the HTML + +### Subsequent Navigation (SPA) + +All routes use server `loader` for both SSR and SPA navigation: + +```text +User clicks link → React Router intercepts + ↓ + Browser makes fetch() to server + ↓ + MRT Server receives request + ↓ + Same loader() runs on server + ↓ + SCAPI requests on MRT + ↓ + JSON response → Browser + ↓ + React updates DOM +``` + +**Key Point:** The loader function code is identical for both SSR and SPA navigation. The only difference is the response format (HTML vs JSON). This is why SCAPI credentials stay secure and MRT orchestration works consistently. + +**Reference:** See README-DATA.md for complete data fetching documentation. diff --git a/packages/b2c-dx-mcp/content/extensions.md b/packages/b2c-dx-mcp/content/extensions.md new file mode 100644 index 00000000..f656d31c --- /dev/null +++ b/packages/b2c-dx-mcp/content/extensions.md @@ -0,0 +1,80 @@ +# Extension Development + +## Structure + +``` +src/extensions/my-extension/ +├── plugin-config.json # Plugin configuration +├── components/ # Extension components +├── routes/ # Extension routes +├── locales/ # Extension translations +└── providers/ # Extension providers +``` + +## Plugin Configuration + +**Insert component into plugin point**: + +```json +{ + "components": [ + { + "pluginId": "header.before.cart", + "path": "extensions/my-extension/components/badge.tsx", + "order": 0 + } + ], + "contextProviders": [ + { + "path": "extensions/my-extension/providers/my-provider.tsx", + "order": 0 + } + ] +} +``` + +## Extension Routes + +Files in `routes/` auto-register: + +```typescript +// src/extensions/my-extension/routes/my-route.tsx +export function loader() { + return { message: 'Hello' }; +} + +export default function MyRoute() { + const { message } = useLoaderData(); + return
{message}
; +} +``` + +## Extension Translations + +Auto-namespaced as `extPascalCase`: + +``` +src/extensions/my-extension/locales/ +├── en-US/translations.json +└── it-IT/translations.json +``` + +```typescript +const {t} = useTranslation('extMyExtension'); +t('welcome'); +``` + +## Integration Markers + +```typescript +// Single line +/** @sfdc-extension-line SFDC_EXT_MY_FEATURE */ +import myFeature from '@extensions/my-feature'; + +// Block +{/* @sfdc-extension-block-start SFDC_EXT_MY_FEATURE */} +My Feature +{/* @sfdc-extension-block-end SFDC_EXT_MY_FEATURE */} +``` + +For full documentation, read: src/extensions/README.md diff --git a/packages/b2c-dx-mcp/content/i18n.md b/packages/b2c-dx-mcp/content/i18n.md new file mode 100644 index 00000000..ec0f4253 --- /dev/null +++ b/packages/b2c-dx-mcp/content/i18n.md @@ -0,0 +1,121 @@ +# Internationalization (i18n) + +## Overview + +- **Server instance**: Has access to all translations for all languages +- **Client instance**: Dynamically imports translations as JavaScript chunks +- **Dual API**: `useTranslation()` for components, `getTranslation()` for everything else + +## Adding Translations + +**In `src/locales/{language}/translations.json`**: + +```json +{ + "product": { + "title": "Product Details", + "addToCart": "Add to Cart", + "greeting": "Hello, {{name}}!", + "itemCount": { + "zero": "No items", + "one": "{{count}} item", + "other": "{{count}} items" + } + } +} +``` + +## Usage + +**2. Use in React components:** + +```typescript +import { useTranslation } from 'react-i18next'; + +export function ProductCard() { + const { t } = useTranslation('product'); + + return ( +
+

{t('title')}

+ +

{t('greeting', { name: 'John' })}

+

{t('itemCount', { count: 5 })}

+
+ ); +} +``` + +**3. Use in non-component code:** + +```typescript +import { getTranslation } from '@/lib/i18next'; + +// Client-side or utilities +const { t } = getTranslation(); +const message = t('product:addToCart'); + +// Server-side (loaders/actions) +export function loader(args: LoaderFunctionArgs) { + const { t } = getTranslation(args.context); + return { title: t('product:title') }; +} +``` + +## Validation Schemas with Translations + +**CRITICAL**: Use factory pattern for Zod schemas to avoid race conditions: + +```typescript +// ❌ WRONG - Module-level schema (race condition) +export const schema = z.object({ + email: z.string().email(t('validation:emailInvalid')) +}); + +// ✅ CORRECT - Factory function +import type { TFunction } from 'i18next'; + +export const createSchema = (t: TFunction) => { + return z.object({ + email: z.string().email(t('validation:emailInvalid')) + }); +}; + +// Usage in component +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function MyForm() { + const { t } = useTranslation(); + const schema = useMemo(() => createSchema(t), [t]); + + const form = useForm({ resolver: zodResolver(schema) }); +} +``` + +## Language Switching + +```typescript +import LocaleSwitcher from '@/components/locale-switcher'; + +export function Footer() { + return
; +} +``` + +## Extension Translations + +Extensions use `extPascalCase` namespace: + +``` +src/extensions/my-extension/locales/ +├── en-US/translations.json +└── it-IT/translations.json +``` + +```typescript +const {t} = useTranslation('extMyExtension'); +t('welcome'); +``` + +**Reference:** See README-I18N.md for complete internationalization documentation. diff --git a/packages/b2c-dx-mcp/content/page-designer.md b/packages/b2c-dx-mcp/content/page-designer.md new file mode 100644 index 00000000..022f523a --- /dev/null +++ b/packages/b2c-dx-mcp/content/page-designer.md @@ -0,0 +1,86 @@ +# Page Designer Integration + +## Overview + +Page Designer is Commerce Cloud's visual editor in Business Manager: merchants build and edit storefront pages (home, category, etc.) without code. The app gets page structure (regions, components, attributes) from the **Shopper Experience API** and renders it via a **component registry** and ``. + +## Concepts + +| Concept | Role | +|--------|------| +| **Page** | Fetched in route loaders via `fetchPageFromLoader(args, { pageId })`. | +| **Region** | Named area on a page; rendered with ``. | +| **Component** | Content block (hero, grid, carousel, etc.) with a `typeId` and attributes; registered in `@/lib/registry`. | +| **Registry** | Static registry in `@/lib/static-registry.ts` is **auto-generated** by the staticRegistry Vite plugin — do not edit by hand. | + +## Which pages use Page Designer + +Only **content pages** that merchants edit in Business Manager use Page Designer; cart, checkout, account, and auth do not. + +| Uses Page Designer | Does not | +|--------------------|----------| +| Home (`pageId: 'homepage'`), Category/PLP (`plp`), Search (`search`), Product/PDP (`pdp`) | Cart, Checkout, Account, Order confirmation, Auth | + +To add a new content page: define a page type and ID in Commerce Cloud, then in your route use `fetchPageFromLoader(args, { pageId })` and `collectComponentDataPromises(args, pagePromise)`, and render `` for each region. + +## Getting started + +### Route (new Page Designer page) + +- **In the loader**: call `fetchPageFromLoader(args, { pageId: '...' })` and `collectComponentDataPromises(args, pagePromise)`; return `page` and `componentData` (keep loaders synchronous; return promises for streaming). +- **In the layout**: for each region, render `` with optional `fallbackElement` and `errorElement` for Suspense/error boundaries. +- **On the route module**: add `@PageType({ name, description, supportedAspectTypes })` and `@RegionDefinition([{ id, name, description, maxComponents }])` so Business Manager knows the page type and regions. Example routes: home (`_app._index.tsx`), category/PLP (`_app.category.$categoryId.tsx`). + +### Component (new Page Designer component) + +- **Add a metadata class** with `@Component('typeId', { name, description })` and `@AttributeDefinition()` (and optionally `@AttributeDefinition({ type: 'image' })`, `type: 'url'`, etc.) for each prop you want editable in Page Designer. Use `@RegionDefinition([...])` if the component has nested regions (e.g. a grid with slots). +- **Implement the React component** so it accepts those props (and strips Page Designer–only props like `component`, `page`, `componentData`, `designMetadata` before spreading to the DOM). If the component needs server data (e.g. products for a carousel), export a `loader({ componentData, context })` and optionally a `fallback` component; the registry calls the loader during `collectComponentDataPromises` and passes resolved data as the `data` prop. +- **Use the MCP tool `storefront_next_design_decorator`** to generate decorators instead of writing them by hand. Example components: `components/hero/index.tsx`, `components/content-card/index.tsx`, `components/product-carousel/index.tsx`. + +### After changes + +- **Rebuild the app** so the static registry (`lib/static-registry.ts`) is regenerated by the staticRegistry Vite plugin. Do not edit the static registry by hand. + +### Design mode + +- **Edit** and **Preview** mode are detected from the request via `isDesignModeActive(request)` and `isPreviewModeActive(request)` from `@salesforce/storefront-next-runtime/design/mode`. The root layout exposes `pageDesignerMode` in loader data (`'EDIT' | 'PREVIEW' | undefined`) so the tree can adapt (e.g. show outlines, disable interactions) when running inside Page Designer. + + +## MCP tools (recommended) + +Use the **B2C DX MCP server** for Page Designer work instead of hand-writing decorators and metadata. Configure the B2C DX MCP server in your IDE (e.g. in MCP settings) so these tools are available. + +### 1. `storefront_next_design_decorator` (STOREFRONTNEXT toolset) + +Adds Page Designer decorators to an existing React component so it can be used in Business Manager. The tool analyzes the component, picks suitable props, infers types (e.g. `*Url`/`*Link` → url, `*Image` → image, `is*`/`show*` → boolean), and generates `@Component('typeId', { name, description })`, `@AttributeDefinition()` on a metadata class, and optionally `@RegionDefinition([...])` for nested regions. It skips complex or UI-only props (e.g. className, style, callbacks). + +- **Auto mode** (fast): Ask in your IDE: *"Add Page Designer support to [ComponentName] with autoMode"*. The tool runs in one turn with no prompts. +- **Interactive mode** (control): Ask *"Add Page Designer support to [ComponentName]"* and answer questions about typeId, which props to expose, types, and optional nested regions. + +### 2. `storefront_next_generate_page_designer_metadata` (STOREFRONTNEXT toolset) + +Generates Page Designer metadata JSON from decorated components, page types, and aspects. Writes files under the cartridge experience folder (e.g. `cartridges/app_storefrontnext_base/cartridge/experience/`). Use after adding or changing decorators so Business Manager has up-to-date component and page-type definitions. + +- **Full scan**: Run with no file list to process the whole project. +- **Incremental**: Pass specific file paths to regenerate only those components. +- **Dry run**: Use `dryRun: true` to see what would be generated without writing files. + +### 3. `cartridge_deploy` (CARTRIDGES toolset) + +Packages the cartridge, uploads it to Commerce Cloud via WebDAV, and unpacks it on the server. Requires Commerce Cloud credentials (e.g. `dw.json` or explicit config). Use after generating metadata so the new/updated metadata is available in Business Manager. + +### Typical workflow + +1. **`storefront_next_design_decorator`** — Add decorators to the component (use autoMode for a quick first pass). +2. **`storefront_next_generate_page_designer_metadata`** — Generate metadata JSON so the component and regions appear in Page Designer. +3. **`cartridge_deploy`** — Deploy to Commerce Cloud so merchants can use the component in Business Manager. + +## Best Practices + +1. **Keep loaders synchronous**: Return promises for Page Designer pages to enable streaming +2. **Use registry for components**: Register all Page Designer components with proper `typeId` +3. **Handle design mode**: Adapt UI when `pageDesignerMode` is `'EDIT'` or `'PREVIEW'` +4. **Rebuild after registry changes**: Static registry is generated at build time +5. **Use MCP tools**: Leverage `storefront_next_design_decorator` and `storefront_next_generate_page_designer_metadata` for faster development + +**Reference:** See README.md for complete Page Designer documentation and MCP tool setup. diff --git a/packages/b2c-dx-mcp/content/performance.md b/packages/b2c-dx-mcp/content/performance.md new file mode 100644 index 00000000..bc5e378d --- /dev/null +++ b/packages/b2c-dx-mcp/content/performance.md @@ -0,0 +1,80 @@ +# Performance Optimization + +## Bundle Size Limits + +The application enforces strict bundle size limits defined in `package.json` under the `bundlesize` configuration. Refer to `package.json` for the complete list of limits. + +**Check bundle size:** + +```bash +pnpm bundlesize:test # Verify limits +pnpm bundlesize:analyze # Analyze composition +``` + +## Built-in Metrics + +Enable in `config.server.ts`: + +```typescript +{ + performance: { + metrics: { + serverPerformanceMetricsEnabled: true, + clientPerformanceMetricsEnabled: true, + serverTimingHeaderEnabled: false // Debug only + } + } +} +``` + +Tracks: + +- SSR operations and rendering time +- SCAPI API calls with parallelization +- Authentication operations +- Client-side navigations + +## Best Practices + +### 1. Parallel Data Fetching + +**Key principle:** Return all promises simultaneously in loaders to enable parallel requests. Avoid sequential `await` calls. + +**Reference:** See `data-fetching` section for detailed parallel vs sequential patterns and code examples. + +### 2. Image Optimization + +Use the `DynamicImage` component with WebP format: + +```typescript +import { DynamicImage } from '@/components/dynamic-image'; + + +``` + +### 3. Progressive Streaming + +**Key principle:** Use synchronous loaders returning promises to enable progressive streaming. Await only critical data, stream the rest. + +**Reference:** See `data-fetching` section for detailed streaming patterns including mixed strategies (awaited + streamed). + +### 4. Lighthouse Audits + +Monitor performance metrics: + +- Preload critical CSS +- Use WebP images by default +- Lazy load below-the-fold content +- Optimize font loading + +```bash +pnpm lighthouse:ci # Run Lighthouse CI +``` + +**Reference:** See README-PERFORMANCE.md for complete performance optimization documentation. diff --git a/packages/b2c-dx-mcp/content/pitfalls.md b/packages/b2c-dx-mcp/content/pitfalls.md new file mode 100644 index 00000000..916a45be --- /dev/null +++ b/packages/b2c-dx-mcp/content/pitfalls.md @@ -0,0 +1,141 @@ +# Common Pitfalls + +## 1. Using Client Loaders/Actions + +```typescript +// ❌ NEVER USE - Client loaders are not permitted +export function clientLoader() { ... } + +// ❌ NEVER USE - Client actions are not permitted +export function clientAction() { ... } + +// ✅ REQUIRED - Server-only data loading +export function loader({ context }: LoaderFunctionArgs) { + const clients = createApiClients(context); + return { product: clients.shopperProducts.getProduct({...}) }; +} + +// ✅ REQUIRED - Server-only actions +export async function action({ request, context }: ActionFunctionArgs) { + const clients = createApiClients(context); + // Handle mutation on server +} +``` + +**Decision tree:** + +```text +Need data for page render? +└─ Use server `loader` + +Need to handle mutations (form submissions, cart updates)? +└─ Use server `action` + +Need on-demand fetching after page load? +└─ Use `useScapiFetcher` (search, modals, infinite scroll) +``` + +**Key Point:** ALL SCAPI requests happen on the server: +- `loader`: Runs on server, SCAPI direct (prod) or proxied (dev) +- `action`: Runs on server, handles mutations securely +- `useScapiFetcher`: Triggers server route that calls SCAPI + +## 2. Module-Level i18n in Schemas + +```typescript +// ❌ RACE CONDITION +const schema = z.object({ + email: z.string().email(t('error')), +}); + +// ✅ FACTORY PATTERN +export const createSchema = (t: TFunction) => { + return z.object({ + email: z.string().email(t('error')), + }); +}; +``` + +## 3. Using Async Loaders (Blocks Page Transitions) + +```typescript +// ❌ BLOCKS PAGE TRANSITIONS - Async loader with await +export async function loader({ context }: LoaderFunctionArgs) { + const product = await fetchProduct(); // Blocks! + const reviews = await fetchReviews(); // Blocks! + return { product, reviews }; +} + +// ✅ NON-BLOCKING - Synchronous loader returning promises +export function loader({ context }: LoaderFunctionArgs): PageData { + return { + product: fetchProduct(), // Streams progressively + reviews: fetchReviews(), // Streams progressively + }; +} +``` + +**Key insight:** Defining loaders as `async` and using `await` causes the entire page transition to block until all data resolves. Use synchronous loaders returning promises for streaming. + +**Reference:** See `data-fetching` section for comprehensive loader patterns including mixed strategies (awaited + streamed). + +## 4. Modifying shadcn/ui + +```typescript +// ❌ NEVER modify src/components/ui/ + +// ✅ Create wrapper +import { Button } from '@/components/ui/button'; +export function MyButton(props) { + return + + +// shadcn/ui: Add via npx shadcn@latest add +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +``` + +**See `styling` section for:** Tailwind CSS 4 rules, Shadcn/ui components, dark mode, responsive design + +--- + +## 🔍 Get Detailed Guidelines + +Use the `storefront_next_development_guidelines` MCP tool with specific sections: + +```json +{ + "sections": ["data-fetching", "components", "testing"] +} +``` + +**Available sections:** +- `data-fetching` - Loaders, actions, useScapiFetcher, data flow +- `components` - createPage HOC, Suspense, file organization +- `styling` - Tailwind CSS 4, Shadcn/ui, styling guidelines +- `testing` - Vitest, Storybook, coverage requirements +- `auth` - Authentication and session management +- `config` - Configuration system +- `i18n` - Internationalization patterns +- `state-management` - Client-side state with Zustand +- `page-designer` - Page Designer integration +- `performance` - Optimization techniques +- `extensions` - Extension development +- `pitfalls` - Common mistakes to avoid + +--- + +**When in doubt:** +1. Check existing code for similar examples +2. Use the MCP tool to get detailed section guidance +3. Follow architectural principles: server-only, streaming, TypeScript diff --git a/packages/b2c-dx-mcp/content/state-management.md b/packages/b2c-dx-mcp/content/state-management.md new file mode 100644 index 00000000..ea6031a5 --- /dev/null +++ b/packages/b2c-dx-mcp/content/state-management.md @@ -0,0 +1,75 @@ +# Client-Side State Management + +## Zustand Store Pattern + +Storefront Next uses Zustand for client-side state (basket, wishlist): + +```typescript +// src/middlewares/basket.client.ts +import {create} from 'zustand'; + +interface BasketStore { + basket: Basket | null; + setBasket: (basket: Basket | null) => void; + clearBasket: () => void; +} + +export const useBasketStore = create((set) => ({ + basket: null, + setBasket: (basket) => set({basket}), + clearBasket: () => set({basket: null}), +})); +``` + +## Context Integration + +Access Zustand state via context helpers: + +```typescript +import { getBasket, updateBasket } from '@/middlewares/basket.client'; + +// In clientLoader +export const clientLoader: ClientLoaderFunction = ({ context }) => { + const basket = getBasket(context); + return { basket, itemCount: basket?.productItems?.length ?? 0 }; +}; + +// In components +function CartIcon() { + const basket = getBasket(context); + return ; +} +``` + +## Update Pattern + +After mutations, update the store: + +```typescript +export async function clientAction({request, context}: ActionFunctionArgs) { + const formData = await request.formData(); + const productId = formData.get('productId') as string; + + const basket = getBasket(context); + const clients = createApiClients(context); + + const {data: updatedBasket} = await clients.shopperBasketsV2.addItemToBasket({ + params: {path: {basketId: basket.basketId}}, + body: [{productId, quantity: 1}], + }); + + // Update Zustand store + updateBasket(context, updatedBasket); + + return Response.json({success: true, basket: updatedBasket}); +} +``` + +## Best Practices + +1. **Use for ephemeral client state**: Shopping cart, UI state, temporary selections +2. **Don't duplicate server state**: Prefer React Router loaders for server data +3. **Keep stores focused**: Separate stores for basket, wishlist, etc. +4. **Sync with server**: Update store after successful mutations + +For full documentation on client-side state management patterns, see the Zustand documentation and React Router state management patterns. diff --git a/packages/b2c-dx-mcp/content/styling.md b/packages/b2c-dx-mcp/content/styling.md new file mode 100644 index 00000000..2e8c06ed --- /dev/null +++ b/packages/b2c-dx-mcp/content/styling.md @@ -0,0 +1,51 @@ +# Styling Guidelines + +## Rules + +- ✅ **Use Tailwind utility classes** in component JSX +- ✅ **Use `cn()` utility** for conditional classes (`import { cn } from '@/lib/utils'`) +- ✅ **Follow mobile-first** responsive patterns (`md:`, `lg:` breakpoints) +- ❌ **NO inline styles** (`style={{...}}`) +- ❌ **NO CSS modules** (`.module.css` files) +- ❌ **NO separate CSS files** for component styles +- ✅ **Custom CSS** only in `src/app.css` for global styles and theme configuration + +## Shadcn/ui Components + +**Adding components:** + +```bash +npx shadcn@latest add +``` + +**Rules:** + +- ✅ **DO** customize components by editing files in `src/components/ui/` +- ❌ **DON'T** create custom components inside `src/components/ui/` +- ❌ **DON'T** manually copy components (use CLI instead) + +## Dark Mode + +Dark mode is supported via CSS variables and the `.dark` class. Theme variables automatically adapt: + +```typescript +
+ +
+``` + +## Responsive Design + +Follow mobile-first responsive design: + +```typescript +
+ {/* Content */} +
+``` + +--- + +**Reference:** See [README-UI-STYLING.md](README-UI-STYLING.md) for complete UI and styling documentation. diff --git a/packages/b2c-dx-mcp/content/testing.md b/packages/b2c-dx-mcp/content/testing.md new file mode 100644 index 00000000..5e01804f --- /dev/null +++ b/packages/b2c-dx-mcp/content/testing.md @@ -0,0 +1,232 @@ +# Testing Strategy + +## Unit Tests (Vitest) + +This project uses **Vitest** for unit tests, running under Vite with jsdom as the default test environment. + +### Test File Organization + +Tests live alongside source files with `.test.ts` or `.test.tsx` extension: + +```typescript +// src/components/product-card/product-card.test.tsx +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { ProductCard } from './product-card'; +import { mockProduct } from '@/test-utils/mocks'; + +describe('ProductCard', () => { + it('renders product name', () => { + render(); + expect(screen.getByText(mockProduct.productName)).toBeInTheDocument(); + }); +}); +``` + +### Test Utilities + +Test utilities are available in `src/test-utils/`: +- `config.ts` - Mock configuration objects and ConfigProvider wrappers +- `context-provider-utils.ts` - Context provider helpers for testing +- `context-provider.tsx` - Test context providers + +### Running Tests + +```bash +# Run all tests with coverage +pnpm test + +# Open Vitest UI (interactive test runner) +pnpm test:ui + +# Watch mode (re-run on file changes) +pnpm test:watch + +# Generate coverage report +pnpm test +# Coverage report outputs to console and coverage/ directory +``` + +### Coverage Requirements + +Coverage thresholds are enforced in `vitest.thresholds.ts`: +- Lines: 73% +- Statements: 73% +- Functions: 86% +- Branches: 87% + +These thresholds represent minimum values that must not be undershot. They should be raised regularly to reflect current status. + +### Testing Libraries + +- **@testing-library/react** - React component testing +- **@testing-library/jest-dom** - Custom Jest DOM matchers +- **@testing-library/user-event** - User interaction simulation +- **@vitest/coverage-v8** - Code coverage +- **@vitest/ui** - Interactive test UI + +## Storybook Testing + +Every reusable component should have a Storybook story file (`.stories.tsx`). + +### Story Structure + +```typescript +// src/components/product-card/product-card.stories.tsx +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { within, expect } from 'storybook/test'; +import { waitForStorybookReady } from '@storybook/test-utils'; +import { ProductCard } from './product-card'; +import { ConfigProvider } from '@/config/context'; +import { mockConfig } from '@/test-utils/config'; +import { mockProduct } from '@/test-utils/mocks'; + +const meta: Meta = { + title: 'Components/ProductCard', + component: ProductCard, + tags: ['autodocs', 'interaction'], + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + product: mockProduct, + }, + play: async ({ canvasElement }) => { + await waitForStorybookReady(canvasElement); + const canvas = within(canvasElement); + await expect(canvas.getByText(mockProduct.productName)).toBeInTheDocument(); + }, +}; +``` + +### Storybook Commands + +```bash +# Development server (port 6006) +pnpm storybook + +# Build static Storybook +pnpm build-storybook + +# Snapshot tests (visual regression) +pnpm test-storybook:snapshot +pnpm test-storybook:snapshot:update # Update snapshots + +# Interaction tests (play functions) +pnpm test-storybook:interaction +pnpm test-storybook:static:interaction # Against static build + +# Accessibility tests +pnpm test-storybook:a11y +pnpm test-storybook:static:a11y # Against static build + +# Generate story tests with coverage +pnpm generate:story-tests:coverage +``` + +### Storybook Features + +- **@storybook/addon-docs** - Automatic documentation generation +- **@storybook/addon-a11y** - Accessibility testing and validation +- **@storybook/addon-vitest** - Integration with Vitest +- **@storybook/test-runner** - Automated testing (interaction, a11y) +- **Viewport Toolbar** - Built-in toolbar for testing different screen sizes + +> **Important**: Use Storybook's built-in viewport toolbar instead of creating separate Mobile/Tablet/Desktop stories. Use the viewport selector in the Storybook toolbar to test components at different screen sizes. + +### Story Tags + +- `autodocs` - Enable automatic documentation +- `interaction` - Include in interaction test runs +- `skip-a11y` - Exclude from a11y tests (use sparingly) + +## Testing Best Practices + +### Component Testing + +1. **Colocate tests** - Keep test files next to source files +2. **Use test utilities** - Leverage `@/test-utils` for mocks and providers +3. **Mock external dependencies** - Use `vi.mock()` for API clients, context providers, etc. +4. **Test user interactions** - Use `@testing-library/user-event` for realistic interactions +5. **Test accessibility** - Use Storybook a11y addon and test-runner + +### Storybook Stories + +1. **Multiple variants** - Create stories for different states (Default, Loading, Error, etc.) +2. **Play functions** - Use `play` functions for interaction testing +3. **Decorators** - Wrap stories with necessary providers (ConfigProvider, etc.) +4. **Documentation** - Include component descriptions and prop documentation +5. **Viewport testing** - Use built-in viewport toolbar, not separate stories + +### Route Testing + +Route tests should mock: +- Loader functions and their return values +- Action functions +- React Router context +- API clients + +Example: + +```typescript +// src/routes/_app.product.$productId.test.tsx +import { describe, test, expect, vi } from 'vitest'; +import { render } from '@testing-library/react'; + +vi.mock('@/components/product-view', () => ({ + default: ({ product }: any) => ( +
+
{product?.name}
+
+ ), +})); + +// Test route component... +``` + +## Testing Recommendations + +### SEO Crawler Emulation + +- Use **Googlebot user agent** in network conditions to emulate SEO crawler behavior +- This changes React Router's streaming strategy and shows what crawlers see for SSR +- Helps verify server-side rendering works correctly for search engines + +### Coverage Goals + +- Maintain coverage above thresholds defined in `vitest.thresholds.ts` +- Raise thresholds regularly as coverage improves +- Focus on testing critical paths and user-facing functionality + +### Test Organization + +``` +src/ +├── components/ +│ ├── product-card/ +│ │ ├── index.tsx # Component +│ │ ├── index.test.tsx # Unit tests +│ │ └── index.stories.tsx # Storybook stories +├── routes/ +│ ├── _app.product.$productId.tsx +│ └── _app.product.$productId.test.tsx +└── test-utils/ # Shared test utilities + ├── config.ts + └── context-provider-utils.ts +``` + +## References + +- **README-TESTS.md** - Complete testing documentation +- **.storybook/README-STORYBOOK.md** - Storybook setup and usage guide +- **vitest.thresholds.ts** - Coverage threshold definitions diff --git a/packages/b2c-dx-mcp/package.json b/packages/b2c-dx-mcp/package.json index 2c02f8da..f8b83eb3 100644 --- a/packages/b2c-dx-mcp/package.json +++ b/packages/b2c-dx-mcp/package.json @@ -39,11 +39,14 @@ "files": [ "bin", "dist", + "content", "oclif.manifest.json", "!dist/**/*.map", - "!dist/**/*.test.*" + "!dist/**/*.test.*", + "!bin/dev.*" ], "exports": { + "./package.json": "./package.json", ".": { "development": "./src/commands/mcp.ts", "import": { diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md b/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md new file mode 100644 index 00000000..710a59bd --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/storefrontnext/README.md @@ -0,0 +1,129 @@ +# Storefront Next Toolset + +MCP tools for Storefront Next development with React Server Components. + +## Tools + +### `storefront_next_development_guidelines` + +**ESSENTIAL FIRST STEP** for Storefront Next development. Returns critical architecture rules, coding standards, and best practices. Use this tool FIRST before writing any Storefront Next code to understand non-negotiable patterns for React Server Components, data loading, and framework constraints. + +**Status**: ✅ Implemented + +**Use cases**: + +- Understand critical rules before writing code +- Learn recommended patterns and conventions +- Get guidance on architecture, data fetching, auth, i18n, components, performance, testing +- Troubleshoot issues and avoid common pitfalls +- Access comprehensive documentation on specific topics + +**Parameters**: + +- `sections` (optional, array): Specific guideline sections to retrieve + - **Default**: `['quick-reference', 'data-fetching', 'components', 'testing']` - Returns comprehensive guidelines covering the most critical topics + - **Single section**: Specify one section name to get focused content + - **Multiple sections**: Specify an array of section names to combine related documentation + - **Empty array**: Returns empty string + - Available section values: + - `quick-reference` - Critical rules, architecture principles, and quick patterns + - `data-fetching` - Data loading patterns with loaders, actions, and useScapiFetcher + - `state-management` - Client-side state management with Zustand + - `auth` - Authentication and session management + - `config` - Configuration system + - `i18n` - Internationalization patterns + - `components` - Component best practices + - `styling` - Tailwind CSS 4, Shadcn/ui, styling guidelines + - `page-designer` - Page Designer integration and component registry + - `performance` - Optimization techniques + - `testing` - Testing strategy + - `extensions` - Framework extensions + - `pitfalls` - Common mistakes to avoid + +**Returns**: Text content with guidelines for the requested section(s) + +**Output format**: + +- **Single section**: Returns content directly (no separators or instructions) +- **Multiple sections**: Returns content with `---` separators between sections, prefixed with instructions to display full content without summarization + +**Example usage**: + +```json +// Default - returns comprehensive guidelines (quick-reference + data-fetching + components + testing) +{ + "name": "storefront_next_development_guidelines" +} + +// Single section +{ + "name": "storefront_next_development_guidelines", + "arguments": { + "sections": ["data-fetching"] + } +} + +// Multiple related sections +{ + "name": "storefront_next_development_guidelines", + "arguments": { + "sections": ["data-fetching", "components", "performance"] + } +} + +// All sections +{ + "name": "storefront_next_development_guidelines", + "arguments": { + "sections": ["quick-reference", "data-fetching", "state-management", "auth", "config", "i18n", "components", "styling", "page-designer", "performance", "testing", "extensions", "pitfalls"] + } +} +``` + +## Implementation Details + +### Architecture + +The tool loads content from markdown files in the `content/` directory: + +- **Content source**: Markdown files loaded at runtime from `packages/b2c-dx-mcp/content/*.md` +- **Quick Reference**: `quick-reference.md` - Critical rules and patterns +- **Section-Based**: Individual markdown files per topic (~100-200 lines each) +- **Default behavior**: Returns 4 sections by default for comprehensive coverage + +### Content Structure + +Each section markdown file includes: + +- Critical rules and best practices +- Code examples (correct ✅ and incorrect ❌ patterns) +- Quick reference snippets +- Framework-specific patterns for React Server Components + +### Behavior + +- **No sections specified**: Returns default comprehensive set (`quick-reference`, `data-fetching`, `components`, `testing`) +- **Single section**: Returns content directly without separators +- **Multiple sections**: Combines content with `---` separators and includes instructions for full content display +- **Empty array**: Returns empty string + +### Benefits + +✅ **Token Efficient**: Returns only relevant content (200-500 lines vs 20K+ full doc) +✅ **Modular**: Access specific sections as needed +✅ **Multi-Select**: Combine related sections in a single call for contextual learning +✅ **Always Current**: Content loaded from markdown files (easy to update) +✅ **Comprehensive Default**: Returns key sections by default for immediate value + +## Placeholder Tools + +The following tools are placeholders awaiting implementation: + +- `storefront_next_site_theming` - Configure and manage site theming for Storefront Next +- `storefront_next_figma_to_component_workflow` - Convert Figma designs to Storefront Next components +- `storefront_next_generate_component` - Generate a new Storefront Next component +- `storefront_next_map_tokens_to_theme` - Map design tokens to Storefront Next theme configuration +- `storefront_next_design_decorator` - Apply design decorators to Storefront Next components +- `storefront_next_generate_page_designer_metadata` - Generate Page Designer metadata for Storefront Next components + +Use `--allow-non-ga-tools` flag to enable placeholder tools. diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/developer-guidelines.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/developer-guidelines.ts new file mode 100644 index 00000000..28381387 --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/storefrontnext/developer-guidelines.ts @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +/** + * Developer Guidelines tool for Storefront Next. + * + * Provides critical development guidelines and best practices for building + * Storefront Next applications with React Server Components. + * + * @module tools/storefrontnext/developer-guidelines + */ + +import {readFileSync} from 'node:fs'; +import {createRequire} from 'node:module'; +import path from 'node:path'; +import {z} from 'zod'; +import type {McpTool} from '../../utils/index.js'; +import type {Services} from '../../services.js'; +import {createToolAdapter, textResult} from '../adapter.js'; + +// Resolve the content directory from the package root +// Uses createRequire to find the package.json location, which is robust +// regardless of where this module is located in the build output +const require = createRequire(import.meta.url); +const packageRoot = path.dirname(require.resolve('@salesforce/b2c-dx-mcp/package.json')); +const CONTENT_DIR = path.join(packageRoot, 'content'); + +/** + * Section metadata with key and optional description. + * Single source of truth for all available sections. + */ +const SECTIONS_METADATA = [ + {key: 'quick-reference', description: null}, // Meta-section, excluded from topics list + { + key: 'data-fetching', + description: + 'server-only data loading (no client loaders), synchronous loaders for streaming, data fetching patterns', + }, + {key: 'state-management', description: 'state management patterns'}, + {key: 'auth', description: 'authentication and session management'}, + {key: 'config', description: 'configuration'}, + {key: 'i18n', description: 'i18n patterns and internationalization'}, + {key: 'components', description: 'component best practices'}, + {key: 'styling', description: 'Tailwind CSS 4, Shadcn/ui, styling guidelines'}, + {key: 'page-designer', description: 'Page Designer integration'}, + {key: 'performance', description: 'performance optimization'}, + {key: 'testing', description: 'testing strategies'}, + {key: 'extensions', description: 'framework extensions'}, + {key: 'pitfalls', description: 'common pitfalls'}, +] as const; + +/** + * Derived: array of section keys for validation. + */ +const _SECTIONS = SECTIONS_METADATA.map((s) => s.key); + +type SectionKey = (typeof SECTIONS_METADATA)[number]['key']; + +/** + * Generates the topics list for the tool description. + * Excludes meta-sections (like quick-reference) that don't have descriptions. + * @returns Comma-separated list of topics + */ +function generateTopicsList(): string { + return SECTIONS_METADATA.filter((s) => s.description !== null) + .map((s) => s.description) + .join(', '); +} + +/** + * Input schema for the developer guidelines tool. + */ +interface DeveloperGuidelinesInput { + sections?: SectionKey[]; +} + +/** + * Detailed section content loaded from markdown files. + * Built dynamically from SECTIONS_METADATA to avoid duplication. + */ +const SECTION_CONTENT: Record = Object.fromEntries( + SECTIONS_METADATA.map((section) => { + const filename = `${section.key}.md`; + const filePath = path.join(CONTENT_DIR, filename); + const content = readFileSync(filePath, 'utf8'); + return [section.key, content]; + }), +) as Record; + +/** + * Default sections to return when no sections are specified. + * Includes quick-reference plus the most critical detailed sections + * to provide comprehensive guidelines by default. + */ +const DEFAULT_SECTIONS: SectionKey[] = ['quick-reference', 'data-fetching', 'components', 'testing']; + +/** + * Creates the developer guidelines tool for Storefront Next. + * + * @param services - MCP services + * @returns The configured MCP tool + */ +export function createDeveloperGuidelinesTool(services: Services): McpTool { + return createToolAdapter( + { + name: 'storefront_next_development_guidelines', + description: + 'ESSENTIAL FIRST STEP for Storefront Next development. Returns critical architecture rules, coding standards, and best practices. ' + + 'Use this tool FIRST before writing any Storefront Next code to understand non-negotiable patterns for React Server Components, ' + + 'data loading, and framework constraints. Returns comprehensive guidelines by default (quick-reference + key sections); ' + + 'supports retrieving specific topic sections. ' + + 'CRITICAL INSTRUCTION: ALWAYS present ALL returned content in FULL - DO NOT SUMMARIZE, DO NOT ADD SUMMARIES, ' + + 'DO NOT ADD OVERVIEWS. The returned content IS the complete answer - display it exactly as provided.', + toolsets: ['STOREFRONTNEXT'], + isGA: false, + requiresInstance: false, + inputSchema: { + sections: z + .array(z.enum([..._SECTIONS] as [string, ...string[]])) + .optional() + .describe( + 'Optional array of specific sections to retrieve. If not specified, returns comprehensive guidelines ' + + '(quick-reference, data-fetching, components, testing). ' + + 'CRITICAL: Present ALL returned content in FULL - DO NOT SUMMARIZE. ' + + 'Available sections: quick-reference, data-fetching, state-management, auth, config, i18n, ' + + 'components, page-designer, performance, testing, extensions, pitfalls. ' + + `Topics covered: ${generateTopicsList()}. ` + + 'Content is complete - present exactly as provided, no summaries.', + ), + }, + async execute(args) { + // Handle empty array case explicitly + if (args.sections && args.sections.length === 0) { + return ''; + } + + // Default to comprehensive set of key sections if no sections specified + const sections = args.sections || DEFAULT_SECTIONS; + + // Multiple sections: combine with separators + const combinedContent = sections.map((section) => SECTION_CONTENT[section]).join('\n\n---\n\n'); + + // Apply instructions for all multi-section responses to ensure full content display + // Single sections are typically smaller and don't need heavy instructions + const isMultiSection = sections.length > 1; + + // Prepend explicit instruction to present full content (not summarized) + // This helps AI assistants understand they should show the complete guidelines without summarization + const fullContentInstruction = isMultiSection + ? '⚠️ CRITICAL: Display the FULL content below. DO NOT summarize, condense, or add overviews.\n\n' + + '📋 STOREFRONT NEXT DEVELOPMENT GUIDELINES\n\n' + + '---\n\n' + : ''; + + // Add footer instruction to reinforce the message for multi-section responses + const footerInstruction = isMultiSection + ? '\n\n---\n\n⚠️ END OF CONTENT - Full content displayed above. Do not add summaries.\n' + : ''; + + // For single sections, return directly (backward compatible) + // For multiple sections, wrap with instructions + if (sections.length === 1) { + return SECTION_CONTENT[sections[0]]; + } + + return fullContentInstruction + combinedContent + footerInstruction; + }, + formatOutput: (output) => textResult(output), + }, + services, + ); +} diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts index e72be0bf..e243a022 100644 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts +++ b/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts @@ -9,9 +9,16 @@ * * This toolset provides MCP tools for Storefront Next development. * - * > ⚠️ **PLACEHOLDER - ACTIVE DEVELOPMENT** - * > Tools in this module are placeholder implementations that return mock responses. - * > Actual implementations are coming soon. Use `--allow-non-ga-tools` flag to enable. + * **Implemented Tools:** + * - `storefront_next_development_guidelines` - Get development guidelines and best practices (GA) + * + * **Placeholder Tools (Use `--allow-non-ga-tools` flag to enable):** + * - `storefront_next_site_theming` - Configure site theming + * - `storefront_next_figma_to_component_workflow` - Convert Figma to components + * - `storefront_next_generate_component` - Generate new components + * - `storefront_next_map_tokens_to_theme` - Map design tokens + * - `storefront_next_design_decorator` - Apply design decorators + * - `storefront_next_generate_page_designer_metadata` - Generate Page Designer metadata * * @module tools/storefrontnext */ @@ -20,6 +27,7 @@ import {z} from 'zod'; import type {McpTool} from '../../utils/index.js'; import type {Services} from '../../services.js'; import {createToolAdapter, jsonResult} from '../adapter.js'; +import {createDeveloperGuidelinesTool} from './developer-guidelines.js'; /** * Common input type for placeholder tools. @@ -91,26 +99,30 @@ function createPlaceholderTool(name: string, description: string, services: Serv */ export function createStorefrontNextTools(services: Services): McpTool[] { return [ + createDeveloperGuidelinesTool(services), createPlaceholderTool( - 'sfnext_development_guidelines', - 'Get Storefront Next development guidelines and best practices', + 'storefront_next_site_theming', + 'Configure and manage site theming for Storefront Next', services, ), - createPlaceholderTool('sfnext_site_theming', 'Configure and manage site theming for Storefront Next', services), createPlaceholderTool( - 'sfnext_figma_to_component_workflow', + 'storefront_next_figma_to_component_workflow', 'Convert Figma designs to Storefront Next components', services, ), - createPlaceholderTool('sfnext_generate_component', 'Generate a new Storefront Next component', services), + createPlaceholderTool('storefront_next_generate_component', 'Generate a new Storefront Next component', services), createPlaceholderTool( - 'sfnext_map_tokens_to_theme', + 'storefront_next_map_tokens_to_theme', 'Map design tokens to Storefront Next theme configuration', services, ), - createPlaceholderTool('sfnext_design_decorator', 'Apply design decorators to Storefront Next components', services), createPlaceholderTool( - 'sfnext_generate_page_designer_metadata', + 'storefront_next_design_decorator', + 'Apply design decorators to Storefront Next components', + services, + ), + createPlaceholderTool( + 'storefront_next_generate_page_designer_metadata', 'Generate Page Designer metadata for Storefront Next components', services, ), diff --git a/packages/b2c-dx-mcp/test/registry.test.ts b/packages/b2c-dx-mcp/test/registry.test.ts index a5f7624d..07e54aca 100644 --- a/packages/b2c-dx-mcp/test/registry.test.ts +++ b/packages/b2c-dx-mcp/test/registry.test.ts @@ -100,9 +100,9 @@ describe('registry', () => { expect(registry.STOREFRONTNEXT.length).to.be.greaterThan(0); const toolNames = registry.STOREFRONTNEXT.map((t) => t.name); - expect(toolNames).to.include('sfnext_development_guidelines'); - expect(toolNames).to.include('sfnext_site_theming'); - expect(toolNames).to.include('sfnext_generate_component'); + expect(toolNames).to.include('storefront_next_development_guidelines'); + expect(toolNames).to.include('storefront_next_site_theming'); + expect(toolNames).to.include('storefront_next_generate_component'); // mrt_bundle_push should also appear in STOREFRONTNEXT (multi-toolset) expect(toolNames).to.include('mrt_bundle_push'); }); @@ -210,7 +210,7 @@ describe('registry', () => { expect(server.registeredTools).to.include('mrt_bundle_push'); expect(server.registeredTools).to.include('pwakit_create_storefront'); expect(server.registeredTools).to.include('scapi_discovery'); - expect(server.registeredTools).to.include('sfnext_development_guidelines'); + expect(server.registeredTools).to.include('storefront_next_development_guidelines'); }); it('should register individual tools via --tools flag', async () => { diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts new file mode 100644 index 00000000..f77c7052 --- /dev/null +++ b/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts @@ -0,0 +1,643 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import {createDeveloperGuidelinesTool} from '../../../src/tools/storefrontnext/developer-guidelines.js'; +import {Services} from '../../../src/services.js'; +import type {ToolResult} from '../../../src/utils/types.js'; + +/** + * Helper to extract text from a ToolResult. + * Throws if the first content item is not a text type. + */ +function getResultText(result: ToolResult): string { + const content = result.content[0]; + if (content.type !== 'text') { + throw new Error(`Expected text content, got ${content.type}`); + } + return content.text; +} + +/** + * Create a mock services instance for testing. + */ +function createMockServices(): Services { + return new Services({}); +} + +describe('tools/storefrontnext/developer-guidelines', () => { + let services: Services; + + beforeEach(() => { + services = createMockServices(); + }); + + describe('tool metadata', () => { + it('should have correct tool name', () => { + const tool = createDeveloperGuidelinesTool(services); + expect(tool.name).to.equal('storefront_next_development_guidelines'); + }); + + it('should have concise, action-oriented description', () => { + const tool = createDeveloperGuidelinesTool(services); + const desc = tool.description; + + // Should emphasize this is an essential first step (most important) + expect(desc).to.include('ESSENTIAL FIRST STEP'); + expect(desc).to.include('Use this tool FIRST'); + + // Should mention core purpose clearly + expect(desc).to.include('Storefront Next'); + expect(desc).to.include('architecture rules'); + expect(desc).to.include('coding standards'); + expect(desc).to.include('best practices'); + + // Should mention key architectural patterns + expect(desc).to.include('React Server Components'); + expect(desc).to.include('data loading'); + expect(desc).to.include('framework constraints'); + + // Should describe behavior concisely + expect(desc).to.match(/comprehensive|quick reference/i); + + // Should be reasonably short (optimized for LLM consumption) + // Note: Description includes critical instructions, so slightly longer than ideal + expect(desc.length).to.be.lessThan(650); + }); + + it('should list all sections in inputSchema description', () => { + const tool = createDeveloperGuidelinesTool(services); + + // The inputSchema should list all available sections for discoverability + // This is better UX than burying them in the main description + const allSections = [ + 'quick-reference', + 'data-fetching', + 'state-management', + 'auth', + 'config', + 'i18n', + 'components', + 'page-designer', + 'performance', + 'testing', + 'extensions', + 'pitfalls', + ]; + + // Each section should be valid (tests that SECTIONS_METADATA is complete) + for (const section of allSections) { + const result = tool.handler({sections: [section]}); + expect(result).to.be.a('promise'); + } + }); + + it('should include detailed topics in inputSchema description', () => { + // Main description should be concise + // Detailed topics should be in inputSchema.sections.describe() + // This follows MCP best practices: main description = WHEN/WHY, inputSchema = HOW + + const tool = createDeveloperGuidelinesTool(services); + const desc = tool.description; + + // Main description should be concise, not list all topics + // Note: Description includes critical instructions, so slightly longer than ideal + expect(desc.length).to.be.lessThan(650); + + // Main description focuses on WHEN and WHY + expect(desc).to.include('ESSENTIAL FIRST STEP'); + expect(desc).to.include('FIRST before writing'); + + // Detailed topics moved to inputSchema (verified by test above) + // This keeps main description scannable for LLMs while providing full detail where needed + }); + + it('should be in STOREFRONTNEXT toolset', () => { + const tool = createDeveloperGuidelinesTool(services); + expect(tool.toolsets).to.include('STOREFRONTNEXT'); + expect(tool.toolsets).to.have.lengthOf(1); + }); + + it('should be GA (generally available)', () => { + const tool = createDeveloperGuidelinesTool(services); + expect(tool.isGA).to.be.false; + }); + + it('should not require B2C instance', () => { + const tool = createDeveloperGuidelinesTool(services); + // Guidelines are static content, no instance needed + expect(tool).to.not.have.property('requiresInstance'); + }); + + it('should prevent section/description mismatch with single source of truth', () => { + // This test ensures that sections and descriptions are defined together + // in SECTIONS_METADATA, making it impossible to have mismatched arrays + + // Verify all 12 sections exist + const allSections = [ + 'quick-reference', + 'data-fetching', + 'state-management', + 'auth', + 'config', + 'i18n', + 'components', + 'page-designer', + 'performance', + 'testing', + 'extensions', + 'pitfalls', + ]; + + // Create tool to verify derived _SECTIONS matches + const tool = createDeveloperGuidelinesTool(services); + + // Each section should be valid and retrievable + for (const section of allSections) { + const result = tool.handler({sections: [section]}); + expect(result).to.be.a('promise'); // Should not throw sync error + } + }); + }); + + describe('inputSchema behavior', () => { + it('should have sections parameter that is optional', async () => { + const tool = createDeveloperGuidelinesTool(services); + + // Should work without providing sections parameter + const result = await tool.handler({}); + expect(result.isError).to.be.undefined; + expect(getResultText(result)).to.not.be.empty; + }); + + it('should accept array of valid section enums', async () => { + const tool = createDeveloperGuidelinesTool(services); + + // All valid sections from _SECTIONS constant + const validSections = [ + 'quick-reference', + 'data-fetching', + 'state-management', + 'auth', + 'config', + 'i18n', + 'components', + 'page-designer', + 'performance', + 'testing', + 'extensions', + 'pitfalls', + ]; + + for (const section of validSections) { + // eslint-disable-next-line no-await-in-loop + const result = await tool.handler({sections: [section]}); + expect(result.isError).to.be.undefined; + } + }); + }); + + describe('default behavior', () => { + it('should return comprehensive guidelines by default when no sections specified', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Verify it returns content (should be non-empty) + expect(text).to.not.be.empty; + + // Should contain quick-reference content + expect(text).to.match(/server|component|data|loading|TypeScript/i); + + // Should contain data-fetching section (comprehensive default) + expect(text).to.include('Data Fetching Patterns'); + + // Should contain components section + expect(text).to.include('Component Patterns'); + + // Should contain testing section + expect(text).to.include('Testing Strategy'); + }); + + it('should return empty string when sections array is explicitly empty', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: []}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + // Empty array returns empty string (edge case) + expect(text).to.be.empty; + }); + }); + + describe('single section retrieval', () => { + it('should support all 12 available sections as documented', () => { + // Verify the tool has exactly 12 sections available + // These are the sections mentioned in the inputSchema description + const expectedSections = [ + 'quick-reference', + 'data-fetching', + 'state-management', + 'auth', + 'config', + 'i18n', + 'components', + 'page-designer', + 'performance', + 'testing', + 'extensions', + 'pitfalls', + ]; + + // This validates the contract stated in the inputSchema + expect(expectedSections).to.have.lengthOf(12); + }); + + it('should return quick-reference section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['quick-reference']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + + it('should return data-fetching section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['data-fetching']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + + it('should return state-management section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['state-management']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + + it('should return auth section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['auth']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + + it('should return config section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['config']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + + it('should return i18n section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['i18n']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + + it('should return components section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['components']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + + it('should return page-designer section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['page-designer']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + + it('should return performance section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['performance']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + + it('should return testing section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['testing']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + + it('should return extensions section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['extensions']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + + it('should return pitfalls section', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['pitfalls']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + expect(text).to.not.be.empty; + }); + }); + + describe('multiple section retrieval', () => { + it('should support contextual learning with multiple sections', async () => { + const tool = createDeveloperGuidelinesTool(services); + + // Test related sections together (as mentioned in description) + const result = await tool.handler({ + sections: ['data-fetching', 'state-management'], + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should contain content from both sections + expect(text).to.not.be.empty; + + // Should contain the separator between sections + expect(text).to.include('\n\n---\n\n'); + + // Content should include topics from both sections + expect(text.toLowerCase()).to.match(/data|fetch|load/); + expect(text.toLowerCase()).to.match(/state|context/); + }); + + it('should combine three sections correctly', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({ + sections: ['auth', 'config', 'i18n'], + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should contain content + expect(text).to.not.be.empty; + + // Should have exactly 2 separators between the 3 content sections + // Plus 2 more from prefix and footer instructions = 4 total + const separators = text.match(/\n\n---\n\n/g); + expect(separators).to.have.lengthOf(4); + }); + + it('should maintain order of sections as requested', async () => { + const tool = createDeveloperGuidelinesTool(services); + + // Request sections in specific order + const result = await tool.handler({ + sections: ['auth', 'config'], + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should contain the separator + expect(text).to.include('\n\n---\n\n'); + + // Split on separator - will include prefix and footer + const allParts = text.split('\n\n---\n\n'); + + // Filter out prefix (starts with warning emoji) and footer (contains "END OF CONTENT") + const contentSections = allParts.filter((part) => !part.includes('⚠️') && !part.includes('END OF CONTENT')); + + // Should have two content sections + expect(contentSections).to.have.lengthOf(2); + + // Verify content is from the expected sections in the correct order + expect(contentSections[0]).to.include('Authentication'); + expect(contentSections[1]).to.include('Configuration'); + }); + + it('should handle all sections at once', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({ + sections: [ + 'quick-reference', + 'data-fetching', + 'state-management', + 'auth', + 'config', + 'i18n', + 'components', + 'page-designer', + 'performance', + 'testing', + 'extensions', + 'pitfalls', + ], + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should contain content + expect(text).to.not.be.empty; + + // Should have at least 11 separators for 12 sections + // (may have more if markdown content contains similar patterns) + const separators = text.match(/\n\n---\n\n/g); + expect(separators).to.not.be.null; + expect(separators!.length).to.be.at.least(11); + + // Verify content from various sections is present + expect(text).to.include('Authentication'); + expect(text).to.include('Configuration'); + expect(text).to.include('Internationalization'); + }); + }); + + describe('input validation', () => { + it('should reject invalid section names', async () => { + const tool = createDeveloperGuidelinesTool(services); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['invalid-section']} as any); + + expect(result.isError).to.be.true; + const text = getResultText(result); + expect(text).to.include('Invalid input'); + }); + + it('should reject empty strings in sections array', async () => { + const tool = createDeveloperGuidelinesTool(services); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: ['']} as any); + + expect(result.isError).to.be.true; + const text = getResultText(result); + expect(text).to.include('Invalid input'); + }); + + it('should reject non-array sections parameter', async () => { + const tool = createDeveloperGuidelinesTool(services); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: 'quick-reference'} as any); + + expect(result.isError).to.be.true; + const text = getResultText(result); + expect(text).to.include('Invalid input'); + }); + }); + + describe('content verification', () => { + it('should load actual markdown content from files', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['quick-reference']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Verify it's actual markdown content (should have markdown formatting) + // Most markdown files have headers, lists, or code blocks + expect(text).to.match(/#|\*|-|```/); + }); + + it('should return different content for different sections', async () => { + const tool = createDeveloperGuidelinesTool(services); + + const result1 = await tool.handler({sections: ['data-fetching']}); + const result2 = await tool.handler({sections: ['auth']}); + + expect(result1.isError).to.be.undefined; + expect(result2.isError).to.be.undefined; + + const text1 = getResultText(result1); + const text2 = getResultText(result2); + + // Different sections should have different content + expect(text1).to.not.equal(text2); + }); + + it('should cover critical topics mentioned in description', async () => { + const tool = createDeveloperGuidelinesTool(services); + + // Test that key topics from the description are covered in relevant sections + const topicTests = [ + {section: 'data-fetching', keywords: ['server', 'data', 'load']}, + {section: 'auth', keywords: ['authentication', 'session']}, + {section: 'i18n', keywords: ['internationalization', 'locale', 'translation']}, + {section: 'performance', keywords: ['performance', 'optimization']}, + {section: 'testing', keywords: ['test']}, + {section: 'pitfalls', keywords: ['pitfall', 'common', 'avoid', 'mistake', 'error']}, + ]; + + for (const {section, keywords} of topicTests) { + // eslint-disable-next-line no-await-in-loop + const result = await tool.handler({sections: [section]}); + expect(result.isError).to.be.undefined; + + const text = getResultText(result).toLowerCase(); + + // At least one keyword should be present + const hasKeyword = keywords.some((keyword) => text.includes(keyword)); + expect(hasKeyword, `Section ${section} should contain one of: ${keywords.join(', ')}`).to.be.true; + } + }); + + it('should provide non-negotiable architecture rules in quick-reference', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['quick-reference']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // The description emphasizes "non-negotiable architecture rules" + // Quick reference should contain guidance about rules/patterns + const hasRulesOrPatterns = + text.toLowerCase().includes('rule') || + text.toLowerCase().includes('pattern') || + text.toLowerCase().includes('must') || + text.toLowerCase().includes('always') || + text.toLowerCase().includes('never'); + + expect(hasRulesOrPatterns, 'Quick reference should contain architecture rules/patterns').to.be.true; + }); + + it('should emphasize TypeScript-only approach', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: ['quick-reference']}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Description mentions "TypeScript-only" + expect(text.toLowerCase()).to.match(/typescript|\.tsx?|type/); + }); + }); + + describe('edge cases', () => { + it('should handle undefined sections parameter', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({sections: undefined}); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should default to comprehensive guidelines (quick-reference + key sections) + expect(text).to.not.be.empty; + expect(text).to.include('Data Fetching Patterns'); + }); + + it('should handle sections parameter explicitly set to null', async () => { + const tool = createDeveloperGuidelinesTool(services); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await tool.handler({sections: null} as any); + + // null is not a valid array, should error + expect(result.isError).to.be.true; + }); + + it('should handle duplicate sections in array', async () => { + const tool = createDeveloperGuidelinesTool(services); + const result = await tool.handler({ + sections: ['auth', 'auth'], + }); + + expect(result.isError).to.be.undefined; + const text = getResultText(result); + + // Should return content with one separator + expect(text).to.include('\n\n---\n\n'); + + // Split on separator - will include prefix and footer, so we need to filter + // The prefix ends with '---\n\n' and footer starts with '\n\n---\n\n' + // So we get: [prefix, section1, section2, footer] + const allParts = text.split('\n\n---\n\n'); + + // Filter out prefix (starts with warning emoji) and footer (contains "END OF CONTENT") + const contentSections = allParts.filter((part) => !part.includes('⚠️') && !part.includes('END OF CONTENT')); + + // Should have duplicated content (same section twice) + expect(contentSections).to.have.lengthOf(2); + // Content should be the same (both are the auth section) + expect(contentSections[0].trim()).to.equal(contentSections[1].trim()); + }); + }); +});