From cc2e0ac1d2188cffdeaaac75d1a668f4398790bf Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 19 Jan 2026 09:37:50 -0500 Subject: [PATCH 1/8] Update VSCode settings and refactor imports in resource.ts for improved organization This commit adds default formatters for TypeScript and JSONC in the VSCode settings, enhancing code formatting consistency. Additionally, it refactors the import statements in resource.ts to consolidate model imports from individual lines into a single block, improving readability and maintainability. --- .vscode/settings.json | 8 ++- .../functions/chat-generate/generateHaiku.ts | 7 -- .../functions/createProduct/createProduct.ts | 4 -- .../generateProductDescription.ts | 4 -- .../generatePriceSuggestion.ts | 8 --- amplify/data/models/index.ts | 27 ++++++++ amplify/data/resource.ts | 64 +++++++------------ 7 files changed, 57 insertions(+), 65 deletions(-) create mode 100644 amplify/data/models/index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 3820a4a6..be4ae2d3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,7 +26,8 @@ "[typescript]": { "editor.codeActionsOnSave": { "source.organizeImports": "never" - } + }, + "editor.defaultFormatter": "vscode.typescript-language-features" }, "[typescriptreact]": { "editor.codeActionsOnSave": { @@ -82,5 +83,8 @@ ], "rust-analyzer.linkedProjects": [ "${workspaceFolder}/packages/liquid-forge-native/Cargo.toml" - ] + ], + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + } } diff --git a/amplify/data/functions/chat-generate/generateHaiku.ts b/amplify/data/functions/chat-generate/generateHaiku.ts index 2934de62..51e03d3f 100644 --- a/amplify/data/functions/chat-generate/generateHaiku.ts +++ b/amplify/data/functions/chat-generate/generateHaiku.ts @@ -2,17 +2,13 @@ import type { Schema } from '../../resource'; import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; import { CHAT_GENERATION_SYSTEM_PROMPT, createChatPrompt, processChatResponse } from './systemPrompt'; -// initialize bedrock runtime client const client = new BedrockRuntimeClient(); export const handler: Schema['generateHaiku']['functionHandler'] = async (event, context) => { - // User prompt const userPrompt = event.arguments.prompt; - // Crear el prompt del usuario usando la función helper const prompt = createChatPrompt(userPrompt); - // Create conversation with the user message const conversation = [ { role: 'user' as const, @@ -20,7 +16,6 @@ export const handler: Schema['generateHaiku']['functionHandler'] = async (event, }, ]; - // Create a command with the model ID, the message, and configuration const command = new ConverseCommand({ modelId: 'anthropic.claude-3-haiku-20240307-v1:0', messages: conversation, @@ -33,9 +28,7 @@ export const handler: Schema['generateHaiku']['functionHandler'] = async (event, const response = await client.send(command); - // Extract the response text const rawResponse = response.output?.message?.content?.[0]?.text || ''; - // Process the response using the helper function return processChatResponse(rawResponse); }; diff --git a/amplify/data/functions/createProduct/createProduct.ts b/amplify/data/functions/createProduct/createProduct.ts index e8a7d4bd..4e54cf09 100644 --- a/amplify/data/functions/createProduct/createProduct.ts +++ b/amplify/data/functions/createProduct/createProduct.ts @@ -23,15 +23,12 @@ export const handler: Schema['createProduct']['functionHandler'] = async (event) owner, } = event.arguments; - // Validaciones básicas if (!name || !nameLowercase || !category || !status || !storeId || !owner) { throw new Error('Invalid arguments'); } - // Generar slug si no se proporciona const finalSlug = slug || nameLowercase.replace(/\s+/g, '-').toLowerCase(); - // Preparar el objeto del producto const productData = { name, nameLowercase, @@ -52,7 +49,6 @@ export const handler: Schema['createProduct']['functionHandler'] = async (event) }; try { - // Insertar el producto en la base de datos usando el cliente de Amplify const { data: createdProduct, errors } = await client.models.Product.create(productData); if (errors && errors.length > 0) { diff --git a/amplify/data/functions/description-generate/generateProductDescription.ts b/amplify/data/functions/description-generate/generateProductDescription.ts index 600ce0e4..62befe48 100644 --- a/amplify/data/functions/description-generate/generateProductDescription.ts +++ b/amplify/data/functions/description-generate/generateProductDescription.ts @@ -7,10 +7,8 @@ const client = new BedrockRuntimeClient(); export const handler: Schema['generateProductDescription']['functionHandler'] = async (event, context) => { const { productName, category } = event.arguments; - // Crear el prompt del usuario usando la función helper const prompt = createUserPrompt(productName, category || undefined); - // Create conversation with the user message const conversation = [ { role: 'user' as const, @@ -18,7 +16,6 @@ export const handler: Schema['generateProductDescription']['functionHandler'] = }, ]; - // Create a command with the model ID, the message, and configuration const command = new ConverseCommand({ modelId: 'us.anthropic.claude-3-haiku-20240307-v1:0', messages: conversation, @@ -31,6 +28,5 @@ export const handler: Schema['generateProductDescription']['functionHandler'] = const response = await client.send(command); - // Extract and return the response text return response.output?.message?.content?.[0]?.text || ''; }; diff --git a/amplify/data/functions/price-suggestion/generatePriceSuggestion.ts b/amplify/data/functions/price-suggestion/generatePriceSuggestion.ts index b5d7d973..16b1ffa4 100644 --- a/amplify/data/functions/price-suggestion/generatePriceSuggestion.ts +++ b/amplify/data/functions/price-suggestion/generatePriceSuggestion.ts @@ -7,18 +7,14 @@ import { createFallbackResponse, } from './systemPrompt'; -// Initialize bedrock runtime client const client = new BedrockRuntimeClient(); export const handler: Schema['generatePriceSuggestion']['functionHandler'] = async (event, context) => { - // Get product details from arguments const { productName, category } = event.arguments; try { - // Crear el prompt del usuario usando la función helper const prompt = createPricePrompt(productName, category || undefined); - // Create conversation with the user message const conversation = [ { role: 'user' as const, @@ -26,7 +22,6 @@ export const handler: Schema['generatePriceSuggestion']['functionHandler'] = asy }, ]; - // Create a command with the model ID, the message, and configuration const command = new ConverseCommand({ modelId: 'us.anthropic.claude-3-haiku-20240307-v1:0', messages: conversation, @@ -39,14 +34,11 @@ export const handler: Schema['generatePriceSuggestion']['functionHandler'] = asy const response = await client.send(command); - // Extract the response text const responseText = response.output?.message?.content?.[0]?.text?.trim() || ''; - // Try to parse the JSON using the helper function const result = parsePriceResponse(responseText); if (result) { - // Return a standardized response return { suggestedPrice: typeof result.suggestedPrice === 'number' ? result.suggestedPrice : 100000, minPrice: typeof result.minPrice === 'number' ? result.minPrice : 90000, diff --git a/amplify/data/models/index.ts b/amplify/data/models/index.ts new file mode 100644 index 00000000..77456dec --- /dev/null +++ b/amplify/data/models/index.ts @@ -0,0 +1,27 @@ +/** + * Barrel file para exportar todos los modelos de Amplify Data + * Esto mantiene el archivo resource.ts limpio y organizado + */ + +export { userProfileModel } from './user-profile'; +export { userSubscriptionModel } from './user-subscription'; +export { userStoreModel } from './user-store'; +export { userThemeModel } from './user-theme'; +export { productModel } from './product'; +export { collectionModel } from './collection'; +export { navigationMenuModel } from './navigation-menu'; +export { pageModel } from './page'; +export { cartModel } from './cart'; +export { cartItemModel } from './cart-item'; +export { checkoutSessionModel } from './checkout-session'; +export { orderModel } from './order'; +export { orderItemModel } from './order-item'; +export { storePaymentConfigModel } from './store-payment-config'; +export { storeCustomDomainModel } from './store-custom-domain'; +export { storeAnalyticsModel } from './store-analytics'; +export { notificationModel } from './notification'; +export { notificationReturnModel } from './notification-return'; +export { productDeleteReturnModel } from './product-delete-return'; +export { orderDeleteReturnModel } from './order-delete-return'; +export { checkoutDeleteReturnModel } from './checkout-delete-return'; +export { websocketConnectionModel } from './websocket-connection'; diff --git a/amplify/data/resource.ts b/amplify/data/resource.ts index fe804c03..1c753724 100644 --- a/amplify/data/resource.ts +++ b/amplify/data/resource.ts @@ -8,30 +8,30 @@ import { planScheduler } from '../functions/planScheduler/resource'; import { webHookPlan } from '../functions/webHookPlan/resource'; import { validateStoreLimits } from '../functions/validateStoreLimits/resource'; import { websocketDevServer } from '../functions/websocket-dev-server/resource'; - -// Importacion de modelos -import { cartModel } from './models/cart'; -import { cartItemModel } from './models/cart-item'; -import { checkoutSessionModel } from './models/checkout-session'; -import { collectionModel } from './models/collection'; -import { navigationMenuModel } from './models/navigation-menu'; -import { orderModel } from './models/order'; -import { orderItemModel } from './models/order-item'; -import { pageModel } from './models/page'; -import { productModel } from './models/product'; -import { storeCustomDomainModel } from './models/store-custom-domain'; -import { storePaymentConfigModel } from './models/store-payment-config'; -import { userProfileModel } from './models/user-profile'; -import { userStoreModel } from './models/user-store'; -import { userSubscriptionModel } from './models/user-subscription'; -import { userThemeModel } from './models/user-theme'; -import { notificationModel } from './models/notification'; -import { notificationReturnModel } from './models/notification-return'; -import { productDeleteReturnModel } from './models/product-delete-return'; -import { orderDeleteReturnModel } from './models/order-delete-return'; -import { checkoutDeleteReturnModel } from './models/checkout-delete-return'; -import { storeAnalyticsModel } from './models/store-analytics'; -import { websocketConnectionModel } from './models/websocket-connection'; +import { + cartModel, + cartItemModel, + checkoutSessionModel, + collectionModel, + navigationMenuModel, + orderModel, + orderItemModel, + pageModel, + productModel, + storeCustomDomainModel, + storePaymentConfigModel, + userProfileModel, + userStoreModel, + userSubscriptionModel, + userThemeModel, + notificationModel, + notificationReturnModel, + productDeleteReturnModel, + orderDeleteReturnModel, + checkoutDeleteReturnModel, + storeAnalyticsModel, + websocketConnectionModel, +} from './models'; import { CHAT_GENERATION_SYSTEM_PROMPT } from './functions/chat-generate/systemPrompt'; export const MODEL_ID = 'us.amazon.nova-pro-v1:0'; @@ -69,7 +69,6 @@ export const createProduct = defineFunction({ timeoutSeconds: 30, }); -// Definir el tipo de entrada para la mutación de pago const PaymentConfigInput = a.customType({ storeId: a.string(), gatewayType: a.string(), @@ -78,16 +77,13 @@ const PaymentConfigInput = a.customType({ isActive: a.boolean(), }); -// Definir el tipo de retorno para la mutación de pago const PaymentConfigResult = a.customType({ success: a.boolean().required(), message: a.string(), }); -// Schema solo para el store (sin funciones de IA) export const storeSchema = a .schema({ - // Solo modelos del store UserProfile: userProfileModel, UserSubscription: userSubscriptionModel, UserStore: userStoreModel, @@ -118,7 +114,6 @@ export const storeSchema = a allow.resource(validateStoreLimits), ]); -// Schema completo incluyendo funciones de IA const fullSchema = a .schema({ chat: a @@ -129,31 +124,21 @@ const fullSchema = a systemPrompt: CHAT_GENERATION_SYSTEM_PROMPT, tools: [ a.ai.dataTool({ - // The name of the tool as it will be referenced in the message to the LLM name: 'ProductQuery', - // The description of the tool provided to the LLM. - // Use this to help the LLM understand when to use the tool. description: 'Searches for Product records', - // A reference to the `a.model()` that the tool will use model: a.ref('Product'), - // The operation to perform on the model modelOperation: 'list', }), a.ai.dataTool({ - // The name of the tool as it will be referenced in the LLM prompt name: 'create_product', - // The description of the tool provided to the LLM. - // Use this to help the LLM understand when to use the tool. description: 'Creates a new product in the store. Use this when the user wants to add a new product to their inventory.', - // A reference to the `a.query()` that the tool will invoke. query: a.ref('createProduct'), }), ], }) .authorization((allow) => allow.owner().to(['create', 'read', 'update', 'delete'])), - // Funciones de IA generateHaiku: a .query() .arguments({ prompt: a.string().required() }) @@ -298,7 +283,6 @@ const fullSchema = a }) ), - // Modelos del store UserProfile: userProfileModel, UserSubscription: userSubscriptionModel, UserStore: userStoreModel, From 1606863acab07f571d38df5405f1e2385b7c2d0d Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 19 Jan 2026 09:42:06 -0500 Subject: [PATCH 2/8] Refactor code formatting and improve VSCode settings for Rust development This commit enhances the VSCode settings by adding a default formatter for Rust files, ensuring consistent code formatting. Additionally, it refactors several Rust benchmark and filter functions for improved readability by consolidating lines and removing unnecessary whitespace. These changes aim to streamline the development process and enhance code maintainability. --- .vscode/settings.json | 9 +++- .../benches/filters_bench.rs | 12 ++--- packages/liquid-forge-native/build.rs | 1 - .../liquid-forge-native/src/filters/html.rs | 13 ++--- .../liquid-forge-native/src/filters/mod.rs | 5 +- .../liquid-forge-native/src/filters/text.rs | 47 ++++++++++++------- packages/liquid-forge-native/src/lib.rs | 1 - 7 files changed, 48 insertions(+), 40 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index be4ae2d3..841c7557 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -79,12 +79,17 @@ "fallbackPolling": "dynamicPriorityPolling" }, "eslint.workingDirectories": [ - { "mode": "auto" } + { + "mode": "auto" + } ], "rust-analyzer.linkedProjects": [ "${workspaceFolder}/packages/liquid-forge-native/Cargo.toml" ], "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features" + }, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" } -} +} \ No newline at end of file diff --git a/packages/liquid-forge-native/benches/filters_bench.rs b/packages/liquid-forge-native/benches/filters_bench.rs index d1d51243..4f40e6e9 100644 --- a/packages/liquid-forge-native/benches/filters_bench.rs +++ b/packages/liquid-forge-native/benches/filters_bench.rs @@ -49,11 +49,7 @@ fn bench_escape(c: &mut Criterion) { }); c.bench_function("escape_html", |b| { - b.iter(|| { - escape(black_box(Some( - "".to_string(), - ))) - }) + b.iter(|| escape(black_box(Some("".to_string())))) }); c.bench_function("escape_mixed", |b| { @@ -104,7 +100,10 @@ fn bench_append(c: &mut Criterion) { let base = "Lorem ipsum ".to_string(); let suffix = "dolor sit amet".to_string(); b.iter(|| { - append(black_box(Some(base.clone())), black_box(Some(suffix.clone()))) + append( + black_box(Some(base.clone())), + black_box(Some(suffix.clone())), + ) }) }); } @@ -131,4 +130,3 @@ criterion_group!( bench_strip_html ); criterion_main!(benches); - diff --git a/packages/liquid-forge-native/build.rs b/packages/liquid-forge-native/build.rs index 8d6c0fc8..37bdddb9 100644 --- a/packages/liquid-forge-native/build.rs +++ b/packages/liquid-forge-native/build.rs @@ -19,4 +19,3 @@ extern crate napi_build; fn main() { napi_build::setup(); } - diff --git a/packages/liquid-forge-native/src/filters/html.rs b/packages/liquid-forge-native/src/filters/html.rs index bdb70c23..141fe4d6 100644 --- a/packages/liquid-forge-native/src/filters/html.rs +++ b/packages/liquid-forge-native/src/filters/html.rs @@ -150,9 +150,7 @@ pub fn strip_newlines(text: Option) -> String { _ => return String::new(), }; - text.chars() - .filter(|c| *c != '\n' && *c != '\r') - .collect() + text.chars().filter(|c| *c != '\n' && *c != '\r').collect() } /// Replaces newlines with HTML `
` tags. @@ -178,8 +176,7 @@ pub fn newline_to_br(text: Option) -> String { _ => return String::new(), }; - text.replace("\r\n", "
") - .replace(['\n', '\r'], "
") + text.replace("\r\n", "
").replace(['\n', '\r'], "
") } #[cfg(test)] @@ -192,10 +189,7 @@ mod tests { escape(Some("".to_string())), "<script>alert('XSS')</script>" ); - assert_eq!( - escape(Some("Rock & Roll".to_string())), - "Rock & Roll" - ); + assert_eq!(escape(Some("Rock & Roll".to_string())), "Rock & Roll"); assert_eq!(escape(None), ""); } @@ -234,4 +228,3 @@ mod tests { assert_eq!(newline_to_br(None), ""); } } - diff --git a/packages/liquid-forge-native/src/filters/mod.rs b/packages/liquid-forge-native/src/filters/mod.rs index b3b75ee4..e06b12e1 100644 --- a/packages/liquid-forge-native/src/filters/mod.rs +++ b/packages/liquid-forge-native/src/filters/mod.rs @@ -19,9 +19,8 @@ //! This module contains high-performance implementations of common //! text processing operations used in Liquid templates. -mod text; mod html; +mod text; -pub use text::*; pub use html::*; - +pub use text::*; diff --git a/packages/liquid-forge-native/src/filters/text.rs b/packages/liquid-forge-native/src/filters/text.rs index 7570d1cd..b7b7188c 100644 --- a/packages/liquid-forge-native/src/filters/text.rs +++ b/packages/liquid-forge-native/src/filters/text.rs @@ -279,7 +279,10 @@ mod tests { #[test] fn test_append() { - assert_eq!(append(Some("Hello".to_string()), Some(" World".to_string())), "Hello World"); + assert_eq!( + append(Some("Hello".to_string()), Some(" World".to_string())), + "Hello World" + ); assert_eq!(append(None, Some("World".to_string())), "World"); assert_eq!(append(Some("Hello".to_string()), None), "Hello"); assert_eq!(append(None, None), ""); @@ -287,7 +290,10 @@ mod tests { #[test] fn test_prepend() { - assert_eq!(prepend(Some("World".to_string()), Some("Hello ".to_string())), "Hello World"); + assert_eq!( + prepend(Some("World".to_string()), Some("Hello ".to_string())), + "Hello World" + ); assert_eq!(prepend(None, Some("Hello".to_string())), "Hello"); assert_eq!(prepend(Some("World".to_string()), None), "World"); } @@ -295,10 +301,22 @@ mod tests { #[test] fn test_handleize() { assert_eq!(handleize(Some("Hello World".to_string())), "hello-world"); - assert_eq!(handleize(Some("Ñoño & Friends".to_string())), "nono-friends"); - assert_eq!(handleize(Some("Café con leche".to_string())), "cafe-con-leche"); - assert_eq!(handleize(Some(" Multiple Spaces ".to_string())), "multiple-spaces"); - assert_eq!(handleize(Some("!!!Exclamation!!!".to_string())), "exclamation"); + assert_eq!( + handleize(Some("Ñoño & Friends".to_string())), + "nono-friends" + ); + assert_eq!( + handleize(Some("Café con leche".to_string())), + "cafe-con-leche" + ); + assert_eq!( + handleize(Some(" Multiple Spaces ".to_string())), + "multiple-spaces" + ); + assert_eq!( + handleize(Some("!!!Exclamation!!!".to_string())), + "exclamation" + ); assert_eq!(handleize(None), ""); } @@ -308,12 +326,13 @@ mod tests { truncate(Some("Hello World".to_string()), Some(8), None), "Hello..." ); + assert_eq!(truncate(Some("Short".to_string()), Some(50), None), "Short"); assert_eq!( - truncate(Some("Short".to_string()), Some(50), None), - "Short" - ); - assert_eq!( - truncate(Some("Hello World".to_string()), Some(8), Some("…".to_string())), + truncate( + Some("Hello World".to_string()), + Some(8), + Some("…".to_string()) + ), "Hello W…" ); } @@ -331,10 +350,7 @@ mod tests { #[test] fn test_default_value() { - assert_eq!( - default_value(None, "N/A".to_string()), - "N/A" - ); + assert_eq!(default_value(None, "N/A".to_string()), "N/A"); assert_eq!( default_value(Some("".to_string()), "N/A".to_string()), "N/A" @@ -345,4 +361,3 @@ mod tests { ); } } - diff --git a/packages/liquid-forge-native/src/lib.rs b/packages/liquid-forge-native/src/lib.rs index 4bf3936b..c7d3cfa5 100644 --- a/packages/liquid-forge-native/src/lib.rs +++ b/packages/liquid-forge-native/src/lib.rs @@ -29,4 +29,3 @@ extern crate napi_derive; mod filters; pub use filters::*; - From 1f7f96e24cb55d66a4e28ac5631941ae84f00a60 Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 21 Jan 2026 13:23:32 -0500 Subject: [PATCH 3/8] Add Claude Code documentation with architecture and development guidelines Created comprehensive CLAUDE.md file to help future Claude Code instances understand the codebase architecture, including: - Complete command reference for development, testing, and deployment - Multi-tenant architecture with DynamoDB sharding strategy - Workspace structure and package organization - Critical development patterns and workflows - Code style guidelines from Cursor rules Co-Authored-By: Claude Sonnet 4.5 --- .claude/CLAUDE.md | 598 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 598 insertions(+) create mode 100644 .claude/CLAUDE.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 00000000..b43ba0ae --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,598 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Fasttify is a multi-tenant SaaS platform for creating and managing online stores with a Liquid template engine 100% compatible with Shopify. The project is built with Next.js 16, AWS Amplify Gen2, and uses a monorepo structure with pnpm workspaces. + +## Build & Development Commands + +### Core Commands +```bash +# Install dependencies (required first step) +pnpm install + +# Start AWS Amplify sandbox (local backend) +npx ampx sandbox --identifier --stream-function-logs + +# Development server with Turbopack +pnpm run dev + +# Production build (optimized with Turbopack) +pnpm run build:fast + +# Full production build (includes type-checking) +pnpm run build:full + +# Start production server +pnpm run start +``` + +### Testing & Quality +```bash +# Run all tests +pnpm run test + +# Run tests in watch mode +pnpm run test:watch + +# Generate coverage report +pnpm run test:coverage + +# Type-check entire codebase +pnpm run type-check + +# Fast type-check (skip lib checks) +pnpm run type-check:fast + +# Lint all files +pnpm run lint + +# Auto-fix lint errors +pnpm run lint:fix + +# Lint with zero warnings enforcement +pnpm run lint:check +``` + +### Workspace Management +```bash +# Install all workspace dependencies +pnpm run workspace:install + +# Update all workspace dependencies +pnpm run workspace:update + +# Build all workspace packages +pnpm run build:packages + +# Test all workspace packages +pnpm run test:packages + +# Lint all workspace packages +pnpm run lint:packages +``` + +### Specialized Commands +```bash +# Compile email templates (React Email) +pnpm run email:compile + +# Test email system +pnpm run email:test + +# Theme converter test (Liquid template conversion) +pnpm run theme-converter:test + +# Convert theme (CLI) +pnpm run theme-converter:convert + +# Deploy to AWS Amplify +pnpm run sandbox:deploy + +# View Lambda logs +pnpm run sandbox:logs + +# Upload base template to S3 +pnpm run upload-template + +# Add license headers to files +pnpm run license + +# Check license headers +pnpm run license:check + +# Analyze bundle size (with webpack-bundle-analyzer) +pnpm run analyze + +# Sync templates in real-time +pnpm run template-sync +``` + +### Test Execution Patterns +```bash +# Run a single test file +pnpm run test path/to/test-file.test.ts + +# Run tests matching a pattern +pnpm run test --testNamePattern="pattern" + +# Run tests in specific directory +pnpm run test path/to/directory +``` + +## Architecture Overview + +### Monorepo Structure + +This is a **pnpm workspace monorepo** with 6 packages: + +- **`packages/liquid-forge`**: Core Liquid template engine (Shopify-compatible) +- **`packages/liquid-forge-native`**: High-performance Rust bindings via NAPI-RS +- **`packages/orders-app`**: Order management module +- **`packages/tenant-domains`**: Multi-tenant domain management with CloudFront +- **`packages/theme-editor`**: Monaco-based Liquid template editor +- **`packages/theme-studio`**: Visual theme builder with Shopify Polaris UI + +The root directory contains the main Next.js application. + +### AWS Amplify Backend (Gen2) + +Backend is defined in `amplify/backend.ts` with these key resources: + +**Directory Structure:** +``` +amplify/ +├── backend.ts # Main backend orchestration +├── auth/ # Cognito configuration +├── data/ # GraphQL schema & models +├── functions/ # Lambda functions +├── storage/ # S3 bucket resources +└── config/ # APIs, queues, permissions +``` + +**Key AWS Services:** +- **DynamoDB**: Multi-tenant database (sharded by `storeId`) +- **Cognito**: User authentication with custom attributes +- **Lambda**: Serverless functions (AI, emails, webhooks) +- **S3**: Theme and image storage +- **SES + SQS**: Email queue system +- **CloudFront**: Multi-tenant CDN +- **AppSync**: GraphQL API +- **Bedrock**: AI (Amazon Nova Pro) + +### Multi-Tenant Architecture + +**Critical Concept:** All store data is partitioned by `storeId` + +- Every store-related entity (Product, Order, Cart, etc.) has `storeId` as partition key +- DynamoDB automatically shards data across partitions +- Queries filtered by `storeId` access a single partition (ultra-fast) +- This eliminates "noisy neighbor" problems and scales horizontally +- Each store's data is fully isolated + +**Example:** When querying products, always filter by `storeId`: +```typescript +const products = await client.models.Product.list({ + filter: { storeId: { eq: currentStoreId } } +}); +``` + +### Next.js Application Structure + +**Three main routing groups:** + +1. **`app/(www)/`**: Public website (homepage, pricing, terms) +2. **`app/(setup)/`**: Onboarding flow (login, first-steps, store creation) +3. **`app/store/[slug]/`**: Admin dashboard (authenticated users) + +**Dynamic store rendering:** +- **`app/[store]/page.tsx`**: Multi-tenant storefront rendering +- Renders Liquid templates server-side (SSR) +- Supports custom domains via CloudFront +- Preview mode for theme development + +**API Routes:** +- **`app/api/stores/[storeId]/`**: Store-specific operations (cart, assets) +- **`app/api/checkout/`**: Checkout processing +- **`app/api/domain-validation/`**: Custom domain verification +- **`app/api/themes/`**: Theme management + +### Liquid Template Engine + +**Located in:** `packages/liquid-forge/` + +The engine provides 100% Shopify Liquid compatibility: + +**Key Components:** +- **Singleton engine** (`liquid/engine.ts`): Main LiquidJS wrapper +- **Filters** (`liquid/filters/`): String, HTML, money, e-commerce, cart filters +- **Tags** (`liquid/tags/`): Custom tags (filters, section, paginate, render) +- **Compiler** (`compiler/`): AST generation and template caching +- **Renderers** (`renderers/`): Store-specific rendering with asset extraction + +**Critical Tags:** +- `{% filters storeId: store.id %}`: Auto-generates complete product filter UI +- `{% section %}`: Theme section definitions +- `{% paginate %}`: Pagination support +- `{% render %}`: Template includes + +**Performance Optimization:** +- Hot filters implemented in Rust (`packages/liquid-forge-native/`) +- Compiled templates cached in memory +- Asset collection during rendering (CSS/JS extraction) + +### State Management + +**Zustand stores** in `context/core/`: +- **`useStoreDataStore`**: Current store data with real-time subscriptions +- **`userStore`**: User authentication state +- **`useSubscriptionStore`**: Subscription information + +**TanStack Query** for server state caching. + +## Critical Development Patterns + +### Working with Amplify Data + +**Always use the centralized client:** +```typescript +import { generateClient } from 'aws-amplify/data'; +import type { Schema } from '@/amplify/data/resource'; + +const client = generateClient(); + +// List with filtering +const { data } = await client.models.Product.list({ + filter: { storeId: { eq: storeId } } +}); + +// Create +const { data } = await client.models.Product.create({ + storeId: currentStoreId, + title: 'New Product', + // ... +}); + +// Subscribe to changes +const subscription = client.models.Product.observeQuery({ + filter: { storeId: { eq: storeId } } +}).subscribe({ + next: ({ items }) => { + // Handle updates + } +}); +``` + +### Server vs Client Components + +**Default to Server Components** unless you need: +- Browser APIs (localStorage, window, etc.) +- Event handlers (onClick, onChange, etc.) +- React hooks (useState, useEffect, etc.) +- Real-time subscriptions + +**Client Component marker:** +```typescript +'use client'; + +export function MyClientComponent() { + const [state, setState] = useState(); + // ... +} +``` + +### Dynamic Store Routing + +**Important:** The `[store]` route matches any slug and custom domains. + +**Store resolution logic:** +1. Check if URL is a custom domain (not fasttify.com) +2. If custom domain, query `StoreCustomDomain` by domain +3. If subdomain, query `UserStore` by slug +4. Load store config and render appropriate Liquid template + +**Location:** `app/[store]/src/_lib/controllers/StorePageController.ts` + +### Theme Development + +**Theme structure:** +``` +templates/ +├── layout/ +│ └── theme.liquid # Main layout +├── templates/ +│ ├── index.liquid # Homepage +│ ├── product.liquid # Product page +│ └── collection.liquid # Collection page +├── sections/ # Reusable sections +└── snippets/ # Small reusable components +``` + +**Template context variables:** +- `store`: Current store data +- `product`: Current product (on product pages) +- `collection`: Current collection (on collection pages) +- `cart`: Current cart +- `request`: Request metadata (path, query params) + +### Email System Architecture + +**Two-tier queue system:** + +1. **High-priority queue**: Transactional emails (order confirmation, password reset) +2. **Bulk queue**: Marketing emails + +**Flow:** +``` +Trigger event + ↓ +Lambda function creates message + ↓ +SQS queue (high-priority or bulk) + ↓ +bulkEmailProcessor Lambda + ↓ +AWS SES sends email +``` + +**Email templates:** React Email components in `components/emails/` + +**Lambda function:** `amplify/functions/bulk-email/` + +### Custom Domain Setup + +**Flow:** +1. User adds domain in admin dashboard +2. System generates DNS verification token +3. User adds CNAME record: `_fasttify-verify.` → `` +4. Background job (`checkStoreDomain`) verifies DNS record +5. Once verified, CloudFront distribution updated +6. SSL certificate provisioned via ACM +7. Domain becomes active + +**Key functions:** +- `amplify/functions/checkStoreDomain/` +- `packages/tenant-domains/src/services/CloudFrontTenantManager.ts` + +## Code Style Guidelines + +### TypeScript Rules (from `.cursor/rules/typescript.mdc`) + +- **Prefer interfaces over types** for object definitions +- Use `type` for unions, intersections, and mapped types +- **Avoid `any`**, prefer `unknown` for unknown types +- Use strict TypeScript configuration +- Use explicit return types for public functions +- Use readonly for immutable properties +- Implement proper null checking + +### React Patterns (from `.cursor/rules/react.mdc`) + +- Use functional components over class components +- Keep components small and focused +- Extract reusable logic into custom hooks +- Use composition over inheritance +- Implement proper memoization (useMemo, useCallback) +- Use React.memo for expensive components +- Implement Error Boundaries + +### Next.js Conventions (from `.cursor/rules/nextjs.mdc`) + +- Use Server Components by default +- Mark client components explicitly with `'use client'` +- Wrap client components in Suspense with fallback +- Use dynamic loading for non-critical components +- Minimize use of useEffect and setState +- Use Zod for form validation + +### Clean Code Principles (from `.cursor/rules/clean-code.mdc`) + +- Replace hard-coded values with named constants +- Use meaningful names that reveal purpose +- Each function should do exactly one thing +- Don't repeat yourself (DRY) +- Hide implementation details (encapsulation) +- Write tests before fixing bugs + +## Important Configuration Files + +| File | Purpose | +|------|---------| +| `package.json` | Root workspace definition & scripts | +| `pnpm-workspace.yaml` | Workspace package definitions | +| `amplify/backend.ts` | AWS Amplify backend orchestration | +| `amplify/data/resource.ts` | Complete GraphQL schema | +| `next.config.ts` | Next.js configuration (transpile packages) | +| `tsconfig.json` | TypeScript configuration | +| `amplify.yml` | AWS Amplify deployment pipeline | +| `.env.local` | Local environment variables (not in git) | +| `.env.production` | Production environment variables (generated in CI) | + +## Environment Variables + +**Required for development:** +```bash +# AWS Amplify (auto-generated by sandbox) +AMPLIFY_* + +# Custom variables (copy from .env.example) +NEXT_PUBLIC_APP_URL=http://localhost:3000 +JWT_SECRET=your-secret +CLOUDFRONT_KEY_PAIR_ID=your-key-id +CLOUDFRONT_PRIVATE_KEY=your-private-key +POLAR_ACCESS_TOKEN=your-token +``` + +## Testing Strategy + +**Test location:** `test/` directory and `__tests__/` directories + +**Test framework:** Jest with React Testing Library + +**Key test utilities:** +- `@testing-library/react`: Component testing +- `@testing-library/jest-dom`: Custom matchers +- `jest-environment-jsdom`: Browser environment simulation + +## Common Development Workflows + +### Adding a New Lambda Function + +1. Create function directory: `amplify/functions/my-function/` +2. Add `resource.ts`: + ```typescript + import { defineFunction } from '@aws-amplify/backend'; + + export const myFunction = defineFunction({ + name: 'myFunction', + entry: './handler.ts', + timeoutSeconds: 30, + }); + ``` +3. Create `handler.ts` with Lambda handler +4. Import in `amplify/data/resource.ts` +5. Add to schema if exposing as Query/Mutation + +### Adding a New Data Model + +1. Create model file: `amplify/data/models/my-model.ts` +2. Define model using Amplify schema builder: + ```typescript + import { a } from '@aws-amplify/backend'; + + export const myModel = a.model({ + storeId: a.string().required(), // Always include for multi-tenancy + name: a.string().required(), + // ... other fields + store: a.belongsTo('UserStore', 'storeId'), + }).authorization([ + a.allow.authenticated().to(['read']), + a.allow.owner(), + ]); + ``` +3. Import in `amplify/data/resource.ts` +4. Add to schema: `MyModel: myModel` +5. Regenerate types: Schema updates automatically on sandbox restart + +### Adding a New Liquid Filter + +1. Add filter to appropriate file in `packages/liquid-forge/liquid/filters/` +2. Or create new filter file if it's a new category +3. Register in `packages/liquid-forge/liquid/engine.ts`: + ```typescript + engine.registerFilter('myFilter', (input, ...args) => { + // Filter implementation + return result; + }); + ``` +4. Update type definitions if needed + +### Creating a New Workspace Package + +1. Create directory: `packages/my-package/` +2. Add `package.json`: + ```json + { + "name": "@fasttify/my-package", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts" + } + ``` +3. Add to root `package.json` workspaces array +4. Add to `next.config.ts` transpilePackages if needed +5. Run `pnpm install` to link workspace + +## Deployment + +**AWS Amplify Hosting** handles deployment automatically: + +1. Push to `main` branch +2. Amplify detects push +3. Backend: `npx ampx pipeline-deploy` +4. Frontend: `pnpm run build:fast` +5. Deploy to CloudFront + S3 +6. Live in ~5-10 minutes + +**Deployment configuration:** `amplify.yml` + +**Build optimization:** +- Turbopack for faster builds +- Cleanup of unnecessary node_modules after build +- Multi-stage caching (pnpm store, .next cache, tsbuildinfo) +- NODE_OPTIONS set to 7GB heap size + +## Key Technical Decisions + +### Why Liquid? + +100% Shopify compatibility allows merchants to: +- Use existing Shopify themes as-is +- Migrate stores easily +- Leverage existing theme marketplace + +### Why DynamoDB Sharding? + +- Infinite horizontal scalability +- Isolation between tenants (security) +- Predictable performance per store +- No "noisy neighbor" problems +- Cost-effective at scale + +### Why Monorepo? + +- Shared dependencies and types +- Atomic commits across packages +- Easier code sharing and refactoring +- Single CI/CD pipeline + +### Why Amplify Gen2? + +- Infrastructure as code in TypeScript +- Automatic IAM permissions +- Type-safe schema generation +- Integrated with Next.js +- Managed scaling and security + +## Troubleshooting + +### Amplify Sandbox Issues + +**Problem:** Sandbox fails to start +- Solution: Check AWS credentials, ensure latest `@aws-amplify/backend-cli` +- Solution: Delete `.amplify` directory and restart + +**Problem:** Type errors after schema changes +- Solution: Restart sandbox to regenerate types +- Solution: Run `pnpm run type-check` to verify + +### Build Failures + +**Problem:** Out of memory during build +- Solution: Increase NODE_OPTIONS: `export NODE_OPTIONS='--max-old-space-size=8192'` + +**Problem:** Workspace package not found +- Solution: Run `pnpm install` to relink workspaces + +### Performance Issues + +**Problem:** Slow Liquid rendering +- Solution: Check template caching is enabled +- Solution: Profile with `console.time()` around render calls +- Solution: Consider moving hot filters to Rust implementation + +**Problem:** Large bundle size +- Solution: Run `pnpm run analyze` to identify large dependencies +- Solution: Use dynamic imports for large components + +## Additional Resources + +- **Documentation:** `docs/` directory +- **Architecture docs:** `docs/architecture/` +- **Liquid engine docs:** `docs/engine/` +- **Theme development:** `docs/templates/` +- **Contributing guide:** `CONTRIBUTING.md` +- **Code of conduct:** `CODE_OF_CONDUCT.md` From 2cbd50585ce3b0faceb46a9821ebbaff7e4c99c2 Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 21 Jan 2026 13:47:44 -0500 Subject: [PATCH 4/8] Refactor TypeScript configuration files for improved readability This commit reformats the `tsconfig.json` and `test/tsconfig.json` files to enhance readability by organizing the "include" and "types" arrays into a more structured format. Additionally, a minor adjustment was made to the `jest.config.ts` file to ensure consistent formatting. These changes aim to improve maintainability and clarity in the configuration files. --- .claude/CLAUDE.md | 84 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index b43ba0ae..e0cf9e78 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -9,6 +9,7 @@ Fasttify is a multi-tenant SaaS platform for creating and managing online stores ## Build & Development Commands ### Core Commands + ```bash # Install dependencies (required first step) pnpm install @@ -30,6 +31,7 @@ pnpm run start ``` ### Testing & Quality + ```bash # Run all tests pnpm run test @@ -57,6 +59,7 @@ pnpm run lint:check ``` ### Workspace Management + ```bash # Install all workspace dependencies pnpm run workspace:install @@ -75,6 +78,7 @@ pnpm run lint:packages ``` ### Specialized Commands + ```bash # Compile email templates (React Email) pnpm run email:compile @@ -111,6 +115,7 @@ pnpm run template-sync ``` ### Test Execution Patterns + ```bash # Run a single test file pnpm run test path/to/test-file.test.ts @@ -142,6 +147,7 @@ The root directory contains the main Next.js application. Backend is defined in `amplify/backend.ts` with these key resources: **Directory Structure:** + ``` amplify/ ├── backend.ts # Main backend orchestration @@ -153,6 +159,7 @@ amplify/ ``` **Key AWS Services:** + - **DynamoDB**: Multi-tenant database (sharded by `storeId`) - **Cognito**: User authentication with custom attributes - **Lambda**: Serverless functions (AI, emails, webhooks) @@ -173,9 +180,10 @@ amplify/ - Each store's data is fully isolated **Example:** When querying products, always filter by `storeId`: + ```typescript const products = await client.models.Product.list({ - filter: { storeId: { eq: currentStoreId } } + filter: { storeId: { eq: currentStoreId } }, }); ``` @@ -188,12 +196,14 @@ const products = await client.models.Product.list({ 3. **`app/store/[slug]/`**: Admin dashboard (authenticated users) **Dynamic store rendering:** + - **`app/[store]/page.tsx`**: Multi-tenant storefront rendering - Renders Liquid templates server-side (SSR) - Supports custom domains via CloudFront - Preview mode for theme development **API Routes:** + - **`app/api/stores/[storeId]/`**: Store-specific operations (cart, assets) - **`app/api/checkout/`**: Checkout processing - **`app/api/domain-validation/`**: Custom domain verification @@ -206,6 +216,7 @@ const products = await client.models.Product.list({ The engine provides 100% Shopify Liquid compatibility: **Key Components:** + - **Singleton engine** (`liquid/engine.ts`): Main LiquidJS wrapper - **Filters** (`liquid/filters/`): String, HTML, money, e-commerce, cart filters - **Tags** (`liquid/tags/`): Custom tags (filters, section, paginate, render) @@ -213,12 +224,14 @@ The engine provides 100% Shopify Liquid compatibility: - **Renderers** (`renderers/`): Store-specific rendering with asset extraction **Critical Tags:** + - `{% filters storeId: store.id %}`: Auto-generates complete product filter UI - `{% section %}`: Theme section definitions - `{% paginate %}`: Pagination support - `{% render %}`: Template includes **Performance Optimization:** + - Hot filters implemented in Rust (`packages/liquid-forge-native/`) - Compiled templates cached in memory - Asset collection during rendering (CSS/JS extraction) @@ -226,6 +239,7 @@ The engine provides 100% Shopify Liquid compatibility: ### State Management **Zustand stores** in `context/core/`: + - **`useStoreDataStore`**: Current store data with real-time subscriptions - **`userStore`**: User authentication state - **`useSubscriptionStore`**: Subscription information @@ -237,6 +251,7 @@ The engine provides 100% Shopify Liquid compatibility: ### Working with Amplify Data **Always use the centralized client:** + ```typescript import { generateClient } from 'aws-amplify/data'; import type { Schema } from '@/amplify/data/resource'; @@ -245,7 +260,7 @@ const client = generateClient(); // List with filtering const { data } = await client.models.Product.list({ - filter: { storeId: { eq: storeId } } + filter: { storeId: { eq: storeId } }, }); // Create @@ -257,23 +272,25 @@ const { data } = await client.models.Product.create({ // Subscribe to changes const subscription = client.models.Product.observeQuery({ - filter: { storeId: { eq: storeId } } + filter: { storeId: { eq: storeId } }, }).subscribe({ next: ({ items }) => { // Handle updates - } + }, }); ``` ### Server vs Client Components **Default to Server Components** unless you need: + - Browser APIs (localStorage, window, etc.) - Event handlers (onClick, onChange, etc.) - React hooks (useState, useEffect, etc.) - Real-time subscriptions **Client Component marker:** + ```typescript 'use client'; @@ -288,6 +305,7 @@ export function MyClientComponent() { **Important:** The `[store]` route matches any slug and custom domains. **Store resolution logic:** + 1. Check if URL is a custom domain (not fasttify.com) 2. If custom domain, query `StoreCustomDomain` by domain 3. If subdomain, query `UserStore` by slug @@ -298,6 +316,7 @@ export function MyClientComponent() { ### Theme Development **Theme structure:** + ``` templates/ ├── layout/ @@ -311,6 +330,7 @@ templates/ ``` **Template context variables:** + - `store`: Current store data - `product`: Current product (on product pages) - `collection`: Current collection (on collection pages) @@ -325,6 +345,7 @@ templates/ 2. **Bulk queue**: Marketing emails **Flow:** + ``` Trigger event ↓ @@ -344,6 +365,7 @@ AWS SES sends email ### Custom Domain Setup **Flow:** + 1. User adds domain in admin dashboard 2. System generates DNS verification token 3. User adds CNAME record: `_fasttify-verify.` → `` @@ -353,6 +375,7 @@ AWS SES sends email 7. Domain becomes active **Key functions:** + - `amplify/functions/checkStoreDomain/` - `packages/tenant-domains/src/services/CloudFrontTenantManager.ts` @@ -398,21 +421,22 @@ AWS SES sends email ## Important Configuration Files -| File | Purpose | -|------|---------| -| `package.json` | Root workspace definition & scripts | -| `pnpm-workspace.yaml` | Workspace package definitions | -| `amplify/backend.ts` | AWS Amplify backend orchestration | -| `amplify/data/resource.ts` | Complete GraphQL schema | -| `next.config.ts` | Next.js configuration (transpile packages) | -| `tsconfig.json` | TypeScript configuration | -| `amplify.yml` | AWS Amplify deployment pipeline | -| `.env.local` | Local environment variables (not in git) | -| `.env.production` | Production environment variables (generated in CI) | +| File | Purpose | +| -------------------------- | -------------------------------------------------- | +| `package.json` | Root workspace definition & scripts | +| `pnpm-workspace.yaml` | Workspace package definitions | +| `amplify/backend.ts` | AWS Amplify backend orchestration | +| `amplify/data/resource.ts` | Complete GraphQL schema | +| `next.config.ts` | Next.js configuration (transpile packages) | +| `tsconfig.json` | TypeScript configuration | +| `amplify.yml` | AWS Amplify deployment pipeline | +| `.env.local` | Local environment variables (not in git) | +| `.env.production` | Production environment variables (generated in CI) | ## Environment Variables **Required for development:** + ```bash # AWS Amplify (auto-generated by sandbox) AMPLIFY_* @@ -432,6 +456,7 @@ POLAR_ACCESS_TOKEN=your-token **Test framework:** Jest with React Testing Library **Key test utilities:** + - `@testing-library/react`: Component testing - `@testing-library/jest-dom`: Custom matchers - `jest-environment-jsdom`: Browser environment simulation @@ -442,6 +467,7 @@ POLAR_ACCESS_TOKEN=your-token 1. Create function directory: `amplify/functions/my-function/` 2. Add `resource.ts`: + ```typescript import { defineFunction } from '@aws-amplify/backend'; @@ -451,6 +477,7 @@ POLAR_ACCESS_TOKEN=your-token timeoutSeconds: 30, }); ``` + 3. Create `handler.ts` with Lambda handler 4. Import in `amplify/data/resource.ts` 5. Add to schema if exposing as Query/Mutation @@ -459,19 +486,20 @@ POLAR_ACCESS_TOKEN=your-token 1. Create model file: `amplify/data/models/my-model.ts` 2. Define model using Amplify schema builder: + ```typescript import { a } from '@aws-amplify/backend'; - export const myModel = a.model({ - storeId: a.string().required(), // Always include for multi-tenancy - name: a.string().required(), - // ... other fields - store: a.belongsTo('UserStore', 'storeId'), - }).authorization([ - a.allow.authenticated().to(['read']), - a.allow.owner(), - ]); + export const myModel = a + .model({ + storeId: a.string().required(), // Always include for multi-tenancy + name: a.string().required(), + // ... other fields + store: a.belongsTo('UserStore', 'storeId'), + }) + .authorization([a.allow.authenticated().to(['read']), a.allow.owner()]); ``` + 3. Import in `amplify/data/resource.ts` 4. Add to schema: `MyModel: myModel` 5. Regenerate types: Schema updates automatically on sandbox restart @@ -520,6 +548,7 @@ POLAR_ACCESS_TOKEN=your-token **Deployment configuration:** `amplify.yml` **Build optimization:** + - Turbopack for faster builds - Cleanup of unnecessary node_modules after build - Multi-stage caching (pnpm store, .next cache, tsbuildinfo) @@ -530,6 +559,7 @@ POLAR_ACCESS_TOKEN=your-token ### Why Liquid? 100% Shopify compatibility allows merchants to: + - Use existing Shopify themes as-is - Migrate stores easily - Leverage existing theme marketplace @@ -562,29 +592,35 @@ POLAR_ACCESS_TOKEN=your-token ### Amplify Sandbox Issues **Problem:** Sandbox fails to start + - Solution: Check AWS credentials, ensure latest `@aws-amplify/backend-cli` - Solution: Delete `.amplify` directory and restart **Problem:** Type errors after schema changes + - Solution: Restart sandbox to regenerate types - Solution: Run `pnpm run type-check` to verify ### Build Failures **Problem:** Out of memory during build + - Solution: Increase NODE_OPTIONS: `export NODE_OPTIONS='--max-old-space-size=8192'` **Problem:** Workspace package not found + - Solution: Run `pnpm install` to relink workspaces ### Performance Issues **Problem:** Slow Liquid rendering + - Solution: Check template caching is enabled - Solution: Profile with `console.time()` around render calls - Solution: Consider moving hot filters to Rust implementation **Problem:** Large bundle size + - Solution: Run `pnpm run analyze` to identify large dependencies - Solution: Use dynamic imports for large components From 31558b74daeb895af34e6e2401655faaa8f597ba Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 21 Jan 2026 14:34:10 -0500 Subject: [PATCH 5/8] Update import paths in next-env.d.ts and add new models in index.ts for enhanced functionality This commit modifies the import path in next-env.d.ts to reference the development types directory, ensuring accurate type resolution during development. Additionally, it introduces new model exports for coupon and coupon usage in index.ts, expanding the data model capabilities. --- amplify/data/models/coupon-usage.ts | 55 ++++++++++ amplify/data/models/coupon.ts | 155 ++++++++++++++++++++++++++++ amplify/data/models/index.ts | 2 + next-env.d.ts | 2 +- 4 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 amplify/data/models/coupon-usage.ts create mode 100644 amplify/data/models/coupon.ts diff --git a/amplify/data/models/coupon-usage.ts b/amplify/data/models/coupon-usage.ts new file mode 100644 index 00000000..69025c51 --- /dev/null +++ b/amplify/data/models/coupon-usage.ts @@ -0,0 +1,55 @@ +import { a } from '@aws-amplify/backend'; + +export const couponUsageModel = a + .model({ + couponId: a + .string() + .required() + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), + storeId: a + .string() + .required() + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), + orderId: a.string().authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), + customerId: a + .string() + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), // userId o sessionId + customerEmail: a + .string() + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), + discountAmount: a + .float() + .required() + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), // Monto real descontado + orderSubtotal: a + .float() + .required() + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), // Subtotal al momento de uso + couponCode: a + .string() + .required() + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), // Código desnormalizado para reporting + storeOwner: a + .string() + .required() + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), + + // Relationships + coupon: a + .belongsTo('Coupon', 'couponId') + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), + order: a + .belongsTo('Order', 'orderId') + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), + store: a + .belongsTo('UserStore', 'storeId') + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), + }) + .secondaryIndexes((index) => [ + index('couponId'), + index('storeId'), + index('orderId'), + index('customerId'), + index('storeOwner'), + ]) + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]); diff --git a/amplify/data/models/coupon.ts b/amplify/data/models/coupon.ts new file mode 100644 index 00000000..024bc526 --- /dev/null +++ b/amplify/data/models/coupon.ts @@ -0,0 +1,155 @@ +import { a } from '@aws-amplify/backend'; + +export const couponModel = a + .model({ + code: a + .string() + .required() + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + storeId: a + .string() + .required() + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + + // Discount configuration (v1: solo percentage) + discountType: a.enum(['percentage']), + discountValue: a + .float() + .required() + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), // Para percentage: 0-100 + + // Constraints + minimumPurchaseAmount: a + .float() + .default(0) + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + + // Usage limits + usageLimit: a + .integer() + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), // null = unlimited + usageLimitPerCustomer: a + .integer() + .default(1) + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + currentUsageCount: a + .integer() + .default(0) + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read', 'update']), + ]), + + // Date constraints + startsAt: a + .datetime() + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + endsAt: a + .datetime() + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + + // Product/collection constraints + appliesToAllProducts: a + .boolean() + .default(true) + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + productIds: a + .json() + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), // Array de product IDs + collectionIds: a + .json() + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), // Array de collection IDs + + // Customer constraints + firstTimeCustomersOnly: a + .boolean() + .default(false) + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + + // Status and metadata + isActive: a + .boolean() + .default(true) + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + title: a + .string() + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), // Internal description + description: a + .string() + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + currency: a + .string() + .default('COP') + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + + // Authorization and relationships + storeOwner: a + .string() + .required() + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + store: a + .belongsTo('UserStore', 'storeId') + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]), + usages: a + .hasMany('CouponUsage', 'couponId') + .authorization((allow) => [allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'delete'])]), + }) + .secondaryIndexes((index) => [index('storeId'), index('code'), index('storeOwner')]) + .authorization((allow) => [ + allow.ownerDefinedIn('storeOwner').to(['create', 'read', 'update', 'delete']), + allow.publicApiKey().to(['read']), + ]); diff --git a/amplify/data/models/index.ts b/amplify/data/models/index.ts index 77456dec..9fbf5097 100644 --- a/amplify/data/models/index.ts +++ b/amplify/data/models/index.ts @@ -25,3 +25,5 @@ export { productDeleteReturnModel } from './product-delete-return'; export { orderDeleteReturnModel } from './order-delete-return'; export { checkoutDeleteReturnModel } from './checkout-delete-return'; export { websocketConnectionModel } from './websocket-connection'; +export { couponModel } from './coupon'; +export { couponUsageModel } from './coupon-usage'; diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c7..c4b7818f 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. From 50d6c5c40d316ee3e3b9662aad9ca721767f7b84 Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 22 Jan 2026 12:22:21 -0500 Subject: [PATCH 6/8] Update dependency overrides in package.json and pnpm-lock.yaml for improved compatibility This commit modifies the dependency overrides for 'tar' and 'lodash' in both package.json and pnpm-lock.yaml, upgrading 'tar' to version 7.5.6 and 'lodash' to version 4.17.23. These changes aim to enhance compatibility and ensure the use of more secure and stable package versions. --- package.json | 9 ++++++-- pnpm-lock.yaml | 57 +++++++++++++++++++++++++++----------------------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index d49dd2ca..d8e8795b 100644 --- a/package.json +++ b/package.json @@ -186,14 +186,19 @@ "overrides": { "@types/react": "19.2.2", "@types/react-dom": "19.2.2", - "tar@=7.5.1": ">=7.5.2", + "tar@=7.5.1": ">=7.5.6", + "tar@<=7.5.3": ">=7.5.6", + "tar": ">=7.5.6", "js-yaml@<3.14.2": ">=3.14.2", "js-yaml@>=4.0.0 <4.1.1": ">=4.1.1", "js-yaml": ">=4.1.1", "mdast-util-to-hast@>=13.0.0 <13.2.1": ">=13.2.1", "@smithy/config-resolver@<4.4.0": ">=4.4.0", "diff@<8.0.3": ">=8.0.3", - "tar@<=7.5.2": ">=7.5.3" + "lodash@<=4.17.22": ">=4.17.23", + "lodash": ">=4.17.23", + "lodash-es@<=4.17.22": ">=4.17.23", + "lodash-es": ">=4.17.23" }, "ignoredBuiltDependencies": [ "core-js" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7d5d239..752e7171 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,14 +7,19 @@ settings: overrides: '@types/react': 19.2.2 '@types/react-dom': 19.2.2 - tar@=7.5.1: '>=7.5.2' + tar@=7.5.1: '>=7.5.6' + tar@<=7.5.3: '>=7.5.6' + tar: '>=7.5.6' js-yaml@<3.14.2: '>=3.14.2' js-yaml@>=4.0.0 <4.1.1: '>=4.1.1' js-yaml: '>=4.1.1' mdast-util-to-hast@>=13.0.0 <13.2.1: '>=13.2.1' '@smithy/config-resolver@<4.4.0': '>=4.4.0' diff@<8.0.3: '>=8.0.3' - tar@<=7.5.2: '>=7.5.3' + lodash@<=4.17.22: '>=4.17.23' + lodash: '>=4.17.23' + lodash-es@<=4.17.22: '>=4.17.23' + lodash-es: '>=4.17.23' importers: @@ -7726,8 +7731,8 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -7768,8 +7773,8 @@ packages: lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} log-symbols@3.0.0: resolution: {integrity: sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==} @@ -9436,8 +9441,8 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - tar@7.5.3: - resolution: {integrity: sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==} + tar@7.5.6: + resolution: {integrity: sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==} engines: {node: '>=18'} tarn@3.0.2: @@ -10709,7 +10714,7 @@ snapshots: graphql-mapping-template: 5.0.2 graphql-transformer-common: 5.1.3 hjson: 3.2.2 - lodash: 4.17.21 + lodash: 4.17.23 md5: 2.3.0 object-hash: 3.0.0 ts-dedent: 2.2.0 @@ -10767,7 +10772,7 @@ snapshots: dependencies: '@aws-amplify/core': 6.13.3 '@aws-sdk/types': 3.398.0 - lodash: 4.17.21 + lodash: 4.17.23 tslib: 2.8.1 '@aws-amplify/platform-core@1.10.0(@aws-cdk/cli-plugin-contract@2.181.1)(@aws-sdk/types@3.901.0)(aws-cdk-lib@2.219.0(constructs@10.4.2))(constructs@10.4.2)': @@ -14126,7 +14131,7 @@ snapshots: dependencies: '@babel/types': 7.0.0-beta.4 jsesc: 2.5.2 - lodash: 4.17.21 + lodash: 4.17.23 source-map: 0.5.7 trim-right: 1.0.1 @@ -14489,7 +14494,7 @@ snapshots: '@babel/types@7.0.0-beta.4': dependencies: esutils: 2.0.3 - lodash: 4.17.21 + lodash: 4.17.23 to-fast-properties: 2.0.0 '@babel/types@7.28.4': @@ -14741,7 +14746,7 @@ snapshots: common-tags: 1.8.0 graphql: 15.10.1 import-from: 4.0.0 - lodash: 4.17.21 + lodash: 4.17.23 tslib: 2.3.1 '@graphql-codegen/plugin-helpers@3.1.2(graphql@15.10.1)': @@ -14751,7 +14756,7 @@ snapshots: common-tags: 1.8.2 graphql: 15.10.1 import-from: 4.0.0 - lodash: 4.17.21 + lodash: 4.17.23 tslib: 2.4.1 '@graphql-codegen/plugin-helpers@5.1.1(graphql@15.10.1)': @@ -14761,7 +14766,7 @@ snapshots: common-tags: 1.8.2 graphql: 15.10.1 import-from: 4.0.0 - lodash: 4.17.21 + lodash: 4.17.23 tslib: 2.6.3 '@graphql-codegen/schema-ast@4.1.0(graphql@15.10.1)': @@ -17612,7 +17617,7 @@ snapshots: '@tailwindcss/oxide@4.1.14': dependencies: detect-libc: 2.1.2 - tar: 7.5.3 + tar: 7.5.6 optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.14 '@tailwindcss/oxide-darwin-arm64': 4.1.14 @@ -18124,7 +18129,7 @@ snapshots: graceful-fs: 4.2.11 is-stream: 2.0.1 lazystream: 1.0.1 - lodash: 4.17.21 + lodash: 4.17.23 normalize-path: 3.0.0 readable-stream: 4.7.0 @@ -18304,7 +18309,7 @@ snapshots: babel-types: 6.26.0 detect-indent: 4.0.0 jsesc: 1.3.0 - lodash: 4.17.21 + lodash: 4.17.23 source-map: 0.5.7 trim-right: 1.0.1 @@ -18412,7 +18417,7 @@ snapshots: dependencies: babel-runtime: 6.26.0 esutils: 2.0.3 - lodash: 4.17.21 + lodash: 4.17.23 to-fast-properties: 1.0.3 bail@2.0.2: {} @@ -20886,7 +20891,7 @@ snapshots: get-package-type: 0.1.0 getopts: 2.3.0 interpret: 2.2.0 - lodash: 4.17.21 + lodash: 4.17.23 pg-connection-string: 2.5.0 rechoir: 0.8.0 resolve-from: 5.0.0 @@ -21001,7 +21006,7 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.21: {} + lodash-es@4.17.23: {} lodash.includes@4.3.0: {} @@ -21029,7 +21034,7 @@ snapshots: lodash.uniq@4.5.0: {} - lodash@4.17.21: {} + lodash@4.17.23: {} log-symbols@3.0.0: dependencies: @@ -22366,7 +22371,7 @@ snapshots: dependencies: clsx: 2.1.1 eventemitter3: 4.0.7 - lodash: 4.17.21 + lodash: 4.17.23 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) react-is: 18.3.1 @@ -23032,7 +23037,7 @@ snapshots: transitivePeerDependencies: - react-native-b4a - tar@7.5.3: + tar@7.5.6: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -23663,8 +23668,8 @@ snapshots: dependencies: '@babel/runtime': 7.28.4 '@types/lodash': 4.17.20 - lodash: 4.17.21 - lodash-es: 4.17.21 + lodash: 4.17.23 + lodash-es: 4.17.23 nanoclone: 0.2.1 property-expr: 2.0.6 toposort: 2.0.2 From 574d0ce32e4f98964e7c1dbc4fccb4f166fa54ab Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 22 Jan 2026 13:18:36 -0500 Subject: [PATCH 7/8] Implement Pino logging in SecureLogger and update VSCode settings for improved formatting This commit integrates the Pino logging library into the SecureLogger class, enhancing logging performance and sanitization of user input. The logger is configured for both development and production environments, with pretty-printing enabled in development. Additionally, VSCode settings are updated to use Prettier as the default formatter for TypeScript, JavaScript, and JSONC files, ensuring consistent code formatting across the project. --- .vscode/settings.json | 10 +- jest.setup.ts | 12 ++ lib/utils/secure-logger.js | 93 +++++++-- lib/utils/secure-logger.ts | 94 +++++++-- package.json | 2 + packages/liquid-forge/lib/logger.ts | 1 - pnpm-lock.yaml | 162 ++++++++++++++++ test/unit/security/secure-logger.test.ts | 230 +++++++++-------------- 8 files changed, 428 insertions(+), 176 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 841c7557..302746ae 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,17 +27,19 @@ "editor.codeActionsOnSave": { "source.organizeImports": "never" }, - "editor.defaultFormatter": "vscode.typescript-language-features" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescriptreact]": { "editor.codeActionsOnSave": { "source.organizeImports": "never" - } + }, + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascript]": { "editor.codeActionsOnSave": { "source.organizeImports": "never" - } + }, + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "files.watcherExclude": { "**/.git/**": true, @@ -87,7 +89,7 @@ "${workspaceFolder}/packages/liquid-forge-native/Cargo.toml" ], "[jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer" diff --git a/jest.setup.ts b/jest.setup.ts index 52c5e812..60a26469 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -5,6 +5,18 @@ process.env.DEV_CACHE_ENABLED = 'true'; global.console.warn = jest.fn(); +if (typeof global.setImmediate === 'undefined') { + global.setImmediate = ((fn: (...args: any[]) => void, ...args: any[]) => { + return setTimeout(fn, 0, ...args) as unknown; + }) as typeof setImmediate; +} + +if (typeof global.clearImmediate === 'undefined') { + global.clearImmediate = ((id: any) => { + clearTimeout(id as unknown as ReturnType); + }) as typeof clearImmediate; +} + if (typeof global.structuredClone === 'undefined') { global.structuredClone = (obj: any) => { return JSON.parse(JSON.stringify(obj)); diff --git a/lib/utils/secure-logger.js b/lib/utils/secure-logger.js index 16a13953..4d0229e7 100644 --- a/lib/utils/secure-logger.js +++ b/lib/utils/secure-logger.js @@ -13,9 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import pino from 'pino'; + +const IS_DEVELOPMENT = process.env.NODE_ENV !== 'production'; + +/** + * Configuración de Pino optimizada para Next.js + */ +const pinoConfig = { + level: process.env.LOG_LEVEL || (IS_DEVELOPMENT ? 'debug' : 'info'), + formatters: { + level: (label) => { + return { level: label.toUpperCase() }; + }, + }, + timestamp: pino.stdTimeFunctions.isoTime, + ...(IS_DEVELOPMENT && { + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'HH:MM:ss', + ignore: 'pid,hostname', + singleLine: false, + }, + }, + }), +}; + +const logger = pino(pinoConfig); + /** * Utilidad de logging segura que previene vulnerabilidades de cadenas de formato - * y sanitiza datos de entrada del usuario + * y sanitiza datos de entrada del usuario. Usa Pino para rendimiento óptimo. */ export class SecureLogger { /** @@ -37,12 +68,23 @@ export class SecureLogger { * Sanitizar todos los argumentos */ static sanitizeArgs(args) { - return args.map((arg) => { + const sanitized = {}; + + args.forEach((arg, index) => { if (typeof arg === 'string') { - return this.sanitizeString(arg); + sanitized[`arg${index}`] = this.sanitizeString(arg); + } else if (arg instanceof Error) { + sanitized.error = { + message: arg.message, + stack: arg.stack, + name: arg.name, + }; + } else { + sanitized[`arg${index}`] = arg; } - return arg; }); + + return sanitized; } /** * Log de información con sanitización @@ -50,7 +92,12 @@ export class SecureLogger { static info(message, ...args) { const sanitizedMessage = this.sanitizeString(message); const sanitizedArgs = this.sanitizeArgs(args); - console.log(sanitizedMessage, ...sanitizedArgs); + + if (Object.keys(sanitizedArgs).length > 0) { + logger.info(sanitizedArgs, sanitizedMessage); + } else { + logger.info(sanitizedMessage); + } } /** * Log de errores con sanitización @@ -58,7 +105,12 @@ export class SecureLogger { static error(message, ...args) { const sanitizedMessage = this.sanitizeString(message); const sanitizedArgs = this.sanitizeArgs(args); - console.error(sanitizedMessage, ...sanitizedArgs); + + if (Object.keys(sanitizedArgs).length > 0) { + logger.error(sanitizedArgs, sanitizedMessage); + } else { + logger.error(sanitizedMessage); + } } /** * Log de warnings con sanitización @@ -66,7 +118,12 @@ export class SecureLogger { static warn(message, ...args) { const sanitizedMessage = this.sanitizeString(message); const sanitizedArgs = this.sanitizeArgs(args); - console.warn(sanitizedMessage, ...sanitizedArgs); + + if (Object.keys(sanitizedArgs).length > 0) { + logger.warn(sanitizedArgs, sanitizedMessage); + } else { + logger.warn(sanitizedMessage); + } } /** * Log de debug con sanitización @@ -74,7 +131,12 @@ export class SecureLogger { static debug(message, ...args) { const sanitizedMessage = this.sanitizeString(message); const sanitizedArgs = this.sanitizeArgs(args); - console.debug(sanitizedMessage, ...sanitizedArgs); + + if (Object.keys(sanitizedArgs).length > 0) { + logger.debug(sanitizedArgs, sanitizedMessage); + } else { + logger.debug(sanitizedMessage); + } } /** * Log seguro usando especificadores de formato @@ -82,19 +144,26 @@ export class SecureLogger { */ static secureLog(level, format, ...args) { const sanitizedArgs = this.sanitizeArgs(args); + switch (level) { case 'info': - console.log(format, ...sanitizedArgs); + logger.info(sanitizedArgs, format); break; case 'error': - console.error(format, ...sanitizedArgs); + logger.error(sanitizedArgs, format); break; case 'warn': - console.warn(format, ...sanitizedArgs); + logger.warn(sanitizedArgs, format); break; case 'debug': - console.debug(format, ...sanitizedArgs); + logger.debug(sanitizedArgs, format); break; } } + /** + * Obtener instancia de Pino para uso avanzado + */ + static getLogger() { + return logger; + } } diff --git a/lib/utils/secure-logger.ts b/lib/utils/secure-logger.ts index 6276945f..d3fc6b15 100644 --- a/lib/utils/secure-logger.ts +++ b/lib/utils/secure-logger.ts @@ -14,9 +14,39 @@ * limitations under the License. */ +import pino from 'pino'; + +const IS_DEVELOPMENT = process.env.NODE_ENV !== 'production'; + +/** + * Configuración de Pino optimizada para Next.js + */ +const pinoConfig = { + level: process.env.LOG_LEVEL || (IS_DEVELOPMENT ? 'debug' : 'info'), + formatters: { + level: (label: string) => { + return { level: label.toUpperCase() }; + }, + }, + timestamp: pino.stdTimeFunctions.isoTime, + ...(IS_DEVELOPMENT && { + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'HH:MM:ss', + ignore: 'pid,hostname', + singleLine: false, + }, + }, + }), +}; + +const logger = pino(pinoConfig); + /** * Utilidad de logging segura que previene vulnerabilidades de cadenas de formato - * y sanitiza datos de entrada del usuario + * y sanitiza datos de entrada del usuario. Usa Pino para rendimiento óptimo. */ export class SecureLogger { /** @@ -39,13 +69,24 @@ export class SecureLogger { /** * Sanitizar todos los argumentos */ - private static sanitizeArgs(args: unknown[]): unknown[] { - return args.map((arg) => { + private static sanitizeArgs(args: unknown[]): Record { + const sanitized: Record = {}; + + args.forEach((arg, index) => { if (typeof arg === 'string') { - return this.sanitizeString(arg); + sanitized[`arg${index}`] = this.sanitizeString(arg); + } else if (arg instanceof Error) { + sanitized.error = { + message: arg.message, + stack: arg.stack, + name: arg.name, + }; + } else { + sanitized[`arg${index}`] = arg; } - return arg; }); + + return sanitized; } /** @@ -54,7 +95,12 @@ export class SecureLogger { static info(message: string, ...args: unknown[]): void { const sanitizedMessage = this.sanitizeString(message); const sanitizedArgs = this.sanitizeArgs(args); - console.log(sanitizedMessage, ...sanitizedArgs); + + if (Object.keys(sanitizedArgs).length > 0) { + logger.info(sanitizedArgs, sanitizedMessage); + } else { + logger.info(sanitizedMessage); + } } /** @@ -63,7 +109,12 @@ export class SecureLogger { static error(message: string, ...args: unknown[]): void { const sanitizedMessage = this.sanitizeString(message); const sanitizedArgs = this.sanitizeArgs(args); - console.error(sanitizedMessage, ...sanitizedArgs); + + if (Object.keys(sanitizedArgs).length > 0) { + logger.error(sanitizedArgs, sanitizedMessage); + } else { + logger.error(sanitizedMessage); + } } /** @@ -72,7 +123,12 @@ export class SecureLogger { static warn(message: string, ...args: unknown[]): void { const sanitizedMessage = this.sanitizeString(message); const sanitizedArgs = this.sanitizeArgs(args); - console.warn(sanitizedMessage, ...sanitizedArgs); + + if (Object.keys(sanitizedArgs).length > 0) { + logger.warn(sanitizedArgs, sanitizedMessage); + } else { + logger.warn(sanitizedMessage); + } } /** @@ -81,7 +137,12 @@ export class SecureLogger { static debug(message: string, ...args: unknown[]): void { const sanitizedMessage = this.sanitizeString(message); const sanitizedArgs = this.sanitizeArgs(args); - console.debug(sanitizedMessage, ...sanitizedArgs); + + if (Object.keys(sanitizedArgs).length > 0) { + logger.debug(sanitizedArgs, sanitizedMessage); + } else { + logger.debug(sanitizedMessage); + } } /** @@ -93,17 +154,24 @@ export class SecureLogger { switch (level) { case 'info': - console.log(format, ...sanitizedArgs); + logger.info(sanitizedArgs, format); break; case 'error': - console.error(format, ...sanitizedArgs); + logger.error(sanitizedArgs, format); break; case 'warn': - console.warn(format, ...sanitizedArgs); + logger.warn(sanitizedArgs, format); break; case 'debug': - console.debug(format, ...sanitizedArgs); + logger.debug(sanitizedArgs, format); break; } } + + /** + * Obtener instancia de Pino para uso avanzado + */ + static getLogger() { + return logger; + } } diff --git a/package.json b/package.json index d8e8795b..35d4c757 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "node-cache": "^5.1.2", "node-fetch": "^3.3.2", "ogl": "^1.0.11", + "pino": "^10.2.1", "react": "19.2.0", "react-currency-input-field": "^3.10.0", "react-dom": "19.2.0", @@ -175,6 +176,7 @@ "jest": "^30.0.4", "jest-environment-jsdom": "^30.0.4", "lint-staged": "^16.1.2", + "pino-pretty": "^13.1.3", "postcss": "^8.5.6", "prettier": "^3.6.2", "tailwindcss": "^3.4.17", diff --git a/packages/liquid-forge/lib/logger.ts b/packages/liquid-forge/lib/logger.ts index d50cabe7..976492fb 100644 --- a/packages/liquid-forge/lib/logger.ts +++ b/packages/liquid-forge/lib/logger.ts @@ -142,5 +142,4 @@ export class RendererLogger { } } -// Alias más corto para uso frecuente export const logger = RendererLogger; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 752e7171..045c42da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,6 +199,9 @@ importers: ogl: specifier: ^1.0.11 version: 1.0.11 + pino: + specifier: ^10.2.1 + version: 10.2.1 react: specifier: 19.2.0 version: 19.2.0 @@ -362,6 +365,9 @@ importers: lint-staged: specifier: ^16.1.2 version: 16.2.3 + pino-pretty: + specifier: ^13.1.3 + version: 13.1.3 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -3636,6 +3642,9 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -5438,6 +5447,10 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + auto-bind@4.0.0: resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} engines: {node: '>=8'} @@ -6057,6 +6070,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + debounce-promise@3.1.2: resolution: {integrity: sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==} @@ -6276,6 +6292,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -6550,6 +6569,9 @@ packages: fast-content-type-parse@3.0.0: resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} + fast-copy@4.0.2: + resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} + fast-deep-equal@2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} @@ -6577,6 +6599,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-sha256@1.3.0: resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} @@ -6931,6 +6956,9 @@ packages: header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -7492,6 +7520,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-beautify@1.15.4: resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} engines: {node: '>=14'} @@ -8281,6 +8313,10 @@ packages: ogl@1.0.11: resolution: {integrity: sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -8479,6 +8515,20 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.2.1: + resolution: {integrity: sha512-Tjyv76gdUe2460dEhtcnA4fU/+HhGq2Kr7OWlo2R/Xxbmn/ZNKWavNWTD2k97IE+s755iVU7WcaOEIl+H3cq8w==} + hasBin: true + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -8770,6 +8820,9 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -8792,6 +8845,9 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -8802,6 +8858,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + react-currency-input-field@3.10.0: resolution: {integrity: sha512-GRmZogHh1e1LrmgXg/fKHSuRLYUnj/c/AumfvfuDMA0UX1mDR6u2NR0fzDemRdq4tNHNLucJeJ2OKCr3ehqyDA==} peerDependencies: @@ -8928,6 +8987,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + recharts-scale@0.4.5: resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} @@ -9065,6 +9128,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -9081,6 +9148,9 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} @@ -9182,6 +9252,9 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -9329,6 +9402,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + strnum@1.1.2: resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} @@ -9466,6 +9543,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tildify@2.0.0: resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} engines: {node: '>=8'} @@ -15978,6 +16059,8 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -18245,6 +18328,8 @@ snapshots: at-least-node@1.0.0: {} + atomic-sleep@1.0.0: {} + auto-bind@4.0.0: {} autoprefixer@10.4.21(postcss@8.5.6): @@ -18971,6 +19056,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + dateformat@4.6.3: {} + debounce-promise@3.1.2: {} debounce@1.2.1: {} @@ -19141,6 +19228,10 @@ snapshots: emoji-regex@9.2.2: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -19626,6 +19717,8 @@ snapshots: fast-content-type-parse@3.0.0: {} + fast-copy@4.0.2: {} + fast-deep-equal@2.0.1: {} fast-deep-equal@3.1.3: {} @@ -19654,6 +19747,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-safe-stringify@2.1.1: {} + fast-sha256@1.3.0: {} fast-uri@3.1.0: {} @@ -20045,6 +20140,8 @@ snapshots: capital-case: 1.0.4 tslib: 2.8.1 + help-me@5.0.0: {} + hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -20762,6 +20859,8 @@ snapshots: jiti@2.6.1: {} + joycon@3.1.1: {} + js-beautify@1.15.4: dependencies: config-chain: 1.1.13 @@ -21723,6 +21822,8 @@ snapshots: ogl@1.0.11: {} + on-exit-leak-free@2.1.2: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -21936,6 +22037,42 @@ snapshots: pify@2.3.0: {} + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.3 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.0 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.1.0: {} + + pino@10.2.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 4.0.0 + pirates@4.0.7: {} pkg-dir@4.2.0: @@ -22190,6 +22327,8 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@5.0.0: {} + process@0.11.10: {} promise@7.3.1: @@ -22210,12 +22349,19 @@ snapshots: proxy-from-env@1.1.0: {} + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} pure-rand@7.0.1: {} queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + react-currency-input-field@3.10.0(react@19.2.0): dependencies: react: 19.2.0 @@ -22363,6 +22509,8 @@ snapshots: readdirp@4.1.2: {} + real-require@0.2.0: {} + recharts-scale@0.4.5: dependencies: decimal.js-light: 2.5.1 @@ -22546,6 +22694,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sax@1.4.1: {} @@ -22560,6 +22710,8 @@ snapshots: scheduler@0.27.0: {} + secure-json-parse@4.1.0: {} + selderee@0.11.0: dependencies: parseley: 0.12.1 @@ -22702,6 +22854,10 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map-support@0.5.13: @@ -22876,6 +23032,8 @@ snapshots: strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + strnum@1.1.2: {} strnum@2.1.1: {} @@ -23069,6 +23227,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tildify@2.0.0: {} timeout-signal@2.0.0: {} diff --git a/test/unit/security/secure-logger.test.ts b/test/unit/security/secure-logger.test.ts index 68d9a7c0..ff1bbfb2 100644 --- a/test/unit/security/secure-logger.test.ts +++ b/test/unit/security/secure-logger.test.ts @@ -1,179 +1,117 @@ -import { SecureLogger } from '@/lib/utils/secure-logger'; - -// Mock console methods to capture output -const mockConsole = { - log: jest.fn(), +const mocks = { + info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), }; -// Replace console methods before tests -const originalConsole = { - log: console.log, - error: console.error, - warn: console.warn, - debug: console.debug, -}; +jest.mock('pino', () => { + const pinoMock: any = jest.fn(() => ({ + info: (...args: any[]) => mocks.info(...args), + error: (...args: any[]) => mocks.error(...args), + warn: (...args: any[]) => mocks.warn(...args), + debug: (...args: any[]) => mocks.debug(...args), + })); + + (pinoMock as any).stdTimeFunctions = { + isoTime: jest.fn(), + }; + + return pinoMock; +}); + +import { SecureLogger } from '@/lib/utils/secure-logger'; describe('SecureLogger', () => { beforeEach(() => { - // Replace console methods with mocks - console.log = mockConsole.log; - console.error = mockConsole.error; - console.warn = mockConsole.warn; - console.debug = mockConsole.debug; - - // Clear mock calls - Object.values(mockConsole).forEach((mock) => mock.mockClear()); + mocks.info.mockClear(); + mocks.error.mockClear(); + mocks.warn.mockClear(); + mocks.debug.mockClear(); }); - afterAll(() => { - // Restore original console methods - console.log = originalConsole.log; - console.error = originalConsole.error; - console.warn = originalConsole.warn; - console.debug = originalConsole.debug; + test('should escape percent signs', () => { + SecureLogger.info('User: %s', 'test%d'); + expect(mocks.info).toHaveBeenCalledWith({ arg0: 'test%%d' }, 'User: %%s'); }); - describe('Format String Protection', () => { - test('should escape percent signs in user input', () => { - const maliciousInput = 'user%dname'; - SecureLogger.info('User logged in: %s', maliciousInput); - - expect(mockConsole.log).toHaveBeenCalledWith('User logged in: %%s', 'user%%dname'); - }); - - test('should escape control characters', () => { - const maliciousInput = 'user\r\nname\t\0'; - SecureLogger.error('Invalid user: %s', maliciousInput); - - expect(mockConsole.error).toHaveBeenCalledWith('Invalid user: %%s', 'user\\r\\nname\\t\\0'); - }); - - test('should handle multiple malicious inputs', () => { - const input1 = '%d%s%x'; - const input2 = '\r\n\t'; - SecureLogger.warn('Multiple inputs: %s %s', input1, input2); - - expect(mockConsole.warn).toHaveBeenCalledWith('Multiple inputs: %%s %%s', '%%d%%s%%x', '\\r\\n\\t'); - }); - - test('should handle non-string inputs safely', () => { - const numberInput = 123; - const objectInput = { key: 'value%d' }; - const nullInput = null; - - SecureLogger.debug('Mixed inputs: %s %s %s', numberInput, objectInput, nullInput); - - expect(mockConsole.debug).toHaveBeenCalledWith('Mixed inputs: %%s %%s %%s', 123, { key: 'value%d' }, null); - }); + test('should escape control characters', () => { + SecureLogger.error('Data: %s', 'test\r\n\t'); + expect(mocks.error).toHaveBeenCalledWith({ arg0: 'test\\r\\n\\t' }, 'Data: %%s'); }); - describe('Message Sanitization', () => { - test('should sanitize message strings', () => { - const maliciousMessage = 'Error in %d: %s'; - SecureLogger.error(maliciousMessage, 'details'); - - expect(mockConsole.error).toHaveBeenCalledWith('Error in %%d: %%s', 'details'); - }); - - test('should preserve legitimate format in secureLog', () => { - SecureLogger.secureLog('error', 'User %s performed action %d', 'john%doe', 42); - - expect(mockConsole.error).toHaveBeenCalledWith('User %s performed action %d', 'john%%doe', 42); - }); + test('should handle multiple arguments', () => { + SecureLogger.warn('Values: %s %s', 'a%d', 'b%s'); + expect(mocks.warn).toHaveBeenCalledWith({ arg0: 'a%%d', arg1: 'b%%s' }, 'Values: %%s %%s'); }); - describe('All Logging Levels', () => { - test('should work with info level', () => { - SecureLogger.info('Info message with %s', 'malicious%d'); - expect(mockConsole.log).toHaveBeenCalledWith('Info message with %%s', 'malicious%%d'); - }); - - test('should work with error level', () => { - SecureLogger.error('Error message with %s', 'malicious%d'); - expect(mockConsole.error).toHaveBeenCalledWith('Error message with %%s', 'malicious%%d'); - }); - - test('should work with warn level', () => { - SecureLogger.warn('Warning message with %s', 'malicious%d'); - expect(mockConsole.warn).toHaveBeenCalledWith('Warning message with %%s', 'malicious%%d'); - }); - - test('should work with debug level', () => { - SecureLogger.debug('Debug message with %s', 'malicious%d'); - expect(mockConsole.debug).toHaveBeenCalledWith('Debug message with %%s', 'malicious%%d'); - }); + test('should work with info level', () => { + SecureLogger.info('Info: %s', 'data%d'); + expect(mocks.info).toHaveBeenCalledWith({ arg0: 'data%%d' }, 'Info: %%s'); }); - describe('secureLog Method', () => { - test('should handle all log levels correctly', () => { - const testCases = [ - { level: 'info' as const, mock: mockConsole.log }, - { level: 'error' as const, mock: mockConsole.error }, - { level: 'warn' as const, mock: mockConsole.warn }, - { level: 'debug' as const, mock: mockConsole.debug }, - ]; - - testCases.forEach(({ level, mock }) => { - mock.mockClear(); - SecureLogger.secureLog(level, 'Test %s message', 'malicious%d'); - expect(mock).toHaveBeenCalledWith('Test %s message', 'malicious%%d'); - }); - }); + test('should work with error level', () => { + SecureLogger.error('Error: %s', 'data%d'); + expect(mocks.error).toHaveBeenCalledWith({ arg0: 'data%%d' }, 'Error: %%s'); }); - describe('Real-world Attack Scenarios', () => { - test('should prevent CloudFront domain injection', () => { - const maliciousDomain = 'evil.com%d%s%x'; - SecureLogger.secureLog('error', 'Error creating tenant for domain %s:', maliciousDomain); + test('should work with warn level', () => { + SecureLogger.warn('Warning: %s', 'data%d'); + expect(mocks.warn).toHaveBeenCalledWith({ arg0: 'data%%d' }, 'Warning: %%s'); + }); - expect(mockConsole.error).toHaveBeenCalledWith('Error creating tenant for domain %s:', 'evil.com%%d%%s%%x'); - }); + test('should work with debug level', () => { + SecureLogger.debug('Debug: %s', 'data%d'); + expect(mocks.debug).toHaveBeenCalledWith({ arg0: 'data%%d' }, 'Debug: %%s'); + }); - test('should prevent certificate ARN injection', () => { - const maliciousArn = 'arn:aws:acm:us-east-1:123456789012:certificate/%d%s'; - SecureLogger.secureLog('error', 'Certificate error for ARN %s:', maliciousArn); + test('should handle secureLog with info', () => { + SecureLogger.secureLog('info', 'Message: %s', 'test%d'); + expect(mocks.info).toHaveBeenCalledWith({ arg0: 'test%%d' }, 'Message: %s'); + }); - expect(mockConsole.error).toHaveBeenCalledWith( - 'Certificate error for ARN %s:', - 'arn:aws:acm:us-east-1:123456789012:certificate/%%d%%s' - ); - }); + test('should handle secureLog with error', () => { + SecureLogger.secureLog('error', 'Error: %s', 'test%d'); + expect(mocks.error).toHaveBeenCalledWith({ arg0: 'test%%d' }, 'Error: %s'); + }); - test('should prevent user ID injection in store operations', () => { - const maliciousUserId = 'user123%d%s\r\n\t'; - SecureLogger.secureLog('info', 'Store operation for user %s:', maliciousUserId); + test('should handle empty strings', () => { + SecureLogger.info('Empty: %s', ''); + expect(mocks.info).toHaveBeenCalledWith({ arg0: '' }, 'Empty: %%s'); + }); - expect(mockConsole.log).toHaveBeenCalledWith('Store operation for user %s:', 'user123%%d%%s\\r\\n\\t'); - }); + test('should handle null and undefined', () => { + SecureLogger.error('Values: %s %s', null, undefined); + expect(mocks.error).toHaveBeenCalledWith({ arg0: null, arg1: undefined }, 'Values: %%s %%s'); + }); - test('should prevent template path injection', () => { - const maliciousPath = '/templates/index.liquid%d\0'; - SecureLogger.secureLog('warn', 'Template not found: %s', maliciousPath); + test('should handle numbers', () => { + SecureLogger.info('Number: %s', 123); + expect(mocks.info).toHaveBeenCalledWith({ arg0: 123 }, 'Number: %%s'); + }); - expect(mockConsole.warn).toHaveBeenCalledWith('Template not found: %s', '/templates/index.liquid%%d\\0'); - }); + test('should handle objects', () => { + const obj = { key: 'value' }; + SecureLogger.debug('Object: %s', obj); + expect(mocks.debug).toHaveBeenCalledWith({ arg0: obj }, 'Object: %%s'); }); - describe('Edge Cases', () => { - test('should handle empty strings', () => { - SecureLogger.info('Empty input: %s', ''); - expect(mockConsole.log).toHaveBeenCalledWith('Empty input: %%s', ''); - }); + test('should handle Error objects', () => { + const error = new Error('Test error'); + SecureLogger.error('Error occurred', error); + expect(mocks.error).toHaveBeenCalledWith( + { error: { message: 'Test error', stack: error.stack, name: 'Error' } }, + 'Error occurred' + ); + }); - test('should handle undefined and null', () => { - SecureLogger.error('Undefined: %s, Null: %s', undefined, null); - expect(mockConsole.error).toHaveBeenCalledWith('Undefined: %%s, Null: %%s', undefined, null); - }); + test('should sanitize message with no args', () => { + SecureLogger.info('Message with %d'); + expect(mocks.info).toHaveBeenCalledWith('Message with %%d'); + }); - test('should handle arrays and objects', () => { - const array = ['item%d', 'item2']; - const obj = { key: 'value%s' }; - - SecureLogger.debug('Complex types: %s %s', array, obj); - expect(mockConsole.debug).toHaveBeenCalledWith('Complex types: %%s %%s', array, obj); - }); + test('should prevent format string injection', () => { + SecureLogger.secureLog('error', 'Domain: %s', 'evil.com%d%s'); + expect(mocks.error).toHaveBeenCalledWith({ arg0: 'evil.com%%d%%s' }, 'Domain: %s'); }); }); From f1549e5907435376026ef4cdf4e082dbed4adbc1 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 23 Jan 2026 12:53:56 -0500 Subject: [PATCH 8/8] Refactor logging configuration and enhance VSCode settings for improved development experience This commit refines the logging configuration in SecureLogger by optimizing the Pino logger setup for better performance and user input sanitization. Additionally, it updates VSCode settings to ensure consistent formatting across TypeScript, JavaScript, and JSONC files, further enhancing the development workflow. --- .../skills/test-driven-development/SKILL.md | 389 +++ .../testing-anti-patterns.md | 317 ++ .../vercel-react-best-practices/AGENTS.md | 2666 +++++++++++++++++ .../vercel-react-best-practices/SKILL.md | 127 + .../rules/advanced-event-handler-refs.md | 55 + .../rules/advanced-use-latest.md | 39 + .../rules/async-api-routes.md | 35 + .../rules/async-defer-await.md | 80 + .../rules/async-dependencies.md | 48 + .../rules/async-parallel.md | 24 + .../rules/async-suspense-boundaries.md | 99 + .../rules/bundle-barrel-imports.md | 59 + .../rules/bundle-conditional.md | 35 + .../rules/bundle-defer-third-party.md | 46 + .../rules/bundle-dynamic-imports.md | 32 + .../rules/bundle-preload.md | 44 + .../rules/client-event-listeners.md | 78 + .../rules/client-localstorage-schema.md | 74 + .../rules/client-passive-event-listeners.md | 48 + .../rules/client-swr-dedup.md | 56 + .../rules/js-batch-dom-css.md | 110 + .../rules/js-cache-function-results.md | 80 + .../rules/js-cache-property-access.md | 28 + .../rules/js-cache-storage.md | 68 + .../rules/js-combine-iterations.md | 32 + .../rules/js-early-exit.md | 50 + .../rules/js-hoist-regexp.md | 45 + .../rules/js-index-maps.md | 37 + .../rules/js-length-check-first.md | 50 + .../rules/js-min-max-loop.md | 82 + .../rules/js-set-map-lookups.md | 24 + .../rules/js-tosorted-immutable.md | 57 + .../rules/rendering-activity.md | 26 + .../rules/rendering-animate-svg-wrapper.md | 38 + .../rules/rendering-conditional-render.md | 32 + .../rules/rendering-content-visibility.md | 38 + .../rules/rendering-hoist-jsx.md | 36 + .../rules/rendering-hydration-no-flicker.md | 72 + .../rules/rendering-svg-precision.md | 28 + .../rules/rendering-usetransition-loading.md | 75 + .../rules/rerender-defer-reads.md | 39 + .../rules/rerender-dependencies.md | 45 + .../rules/rerender-derived-state.md | 29 + .../rules/rerender-functional-setstate.md | 77 + .../rules/rerender-lazy-state-init.md | 56 + .../rules/rerender-memo-with-default-value.md | 36 + .../rules/rerender-memo.md | 44 + .../rerender-simple-expression-in-memo.md | 35 + .../rules/rerender-transitions.md | 40 + .../rules/server-after-nonblocking.md | 73 + .../rules/server-auth-actions.md | 96 + .../rules/server-cache-lru.md | 41 + .../rules/server-cache-react.md | 76 + .../rules/server-dedup-props.md | 65 + .../rules/server-parallel-fetching.md | 83 + .../rules/server-serialization.md | 38 + .../skills/test-driven-development/SKILL.md | 389 +++ .../testing-anti-patterns.md | 317 ++ .../vercel-react-best-practices/AGENTS.md | 2666 +++++++++++++++++ .../vercel-react-best-practices/SKILL.md | 127 + .../rules/advanced-event-handler-refs.md | 55 + .../rules/advanced-use-latest.md | 39 + .../rules/async-api-routes.md | 35 + .../rules/async-defer-await.md | 80 + .../rules/async-dependencies.md | 48 + .../rules/async-parallel.md | 24 + .../rules/async-suspense-boundaries.md | 99 + .../rules/bundle-barrel-imports.md | 59 + .../rules/bundle-conditional.md | 35 + .../rules/bundle-defer-third-party.md | 46 + .../rules/bundle-dynamic-imports.md | 32 + .../rules/bundle-preload.md | 44 + .../rules/client-event-listeners.md | 78 + .../rules/client-localstorage-schema.md | 74 + .../rules/client-passive-event-listeners.md | 48 + .../rules/client-swr-dedup.md | 56 + .../rules/js-batch-dom-css.md | 110 + .../rules/js-cache-function-results.md | 80 + .../rules/js-cache-property-access.md | 28 + .../rules/js-cache-storage.md | 68 + .../rules/js-combine-iterations.md | 32 + .../rules/js-early-exit.md | 50 + .../rules/js-hoist-regexp.md | 45 + .../rules/js-index-maps.md | 37 + .../rules/js-length-check-first.md | 50 + .../rules/js-min-max-loop.md | 82 + .../rules/js-set-map-lookups.md | 24 + .../rules/js-tosorted-immutable.md | 57 + .../rules/rendering-activity.md | 26 + .../rules/rendering-animate-svg-wrapper.md | 38 + .../rules/rendering-conditional-render.md | 32 + .../rules/rendering-content-visibility.md | 38 + .../rules/rendering-hoist-jsx.md | 36 + .../rules/rendering-hydration-no-flicker.md | 72 + .../rules/rendering-svg-precision.md | 28 + .../rules/rendering-usetransition-loading.md | 75 + .../rules/rerender-defer-reads.md | 39 + .../rules/rerender-dependencies.md | 45 + .../rules/rerender-derived-state.md | 29 + .../rules/rerender-functional-setstate.md | 77 + .../rules/rerender-lazy-state-init.md | 56 + .../rules/rerender-memo-with-default-value.md | 36 + .../rules/rerender-memo.md | 44 + .../rerender-simple-expression-in-memo.md | 35 + .../rules/rerender-transitions.md | 40 + .../rules/server-after-nonblocking.md | 73 + .../rules/server-auth-actions.md | 96 + .../rules/server-cache-lru.md | 41 + .../rules/server-cache-react.md | 76 + .../rules/server-dedup-props.md | 65 + .../rules/server-parallel-fetching.md | 83 + .../rules/server-serialization.md | 38 + .../skills/test-driven-development/SKILL.md | 389 +++ .../testing-anti-patterns.md | 317 ++ .../vercel-react-best-practices/AGENTS.md | 2666 +++++++++++++++++ .../vercel-react-best-practices/SKILL.md | 127 + .../rules/advanced-event-handler-refs.md | 55 + .../rules/advanced-use-latest.md | 39 + .../rules/async-api-routes.md | 35 + .../rules/async-defer-await.md | 80 + .../rules/async-dependencies.md | 48 + .../rules/async-parallel.md | 24 + .../rules/async-suspense-boundaries.md | 99 + .../rules/bundle-barrel-imports.md | 59 + .../rules/bundle-conditional.md | 35 + .../rules/bundle-defer-third-party.md | 46 + .../rules/bundle-dynamic-imports.md | 32 + .../rules/bundle-preload.md | 44 + .../rules/client-event-listeners.md | 78 + .../rules/client-localstorage-schema.md | 74 + .../rules/client-passive-event-listeners.md | 48 + .../rules/client-swr-dedup.md | 56 + .../rules/js-batch-dom-css.md | 110 + .../rules/js-cache-function-results.md | 80 + .../rules/js-cache-property-access.md | 28 + .../rules/js-cache-storage.md | 68 + .../rules/js-combine-iterations.md | 32 + .../rules/js-early-exit.md | 50 + .../rules/js-hoist-regexp.md | 45 + .../rules/js-index-maps.md | 37 + .../rules/js-length-check-first.md | 50 + .../rules/js-min-max-loop.md | 82 + .../rules/js-set-map-lookups.md | 24 + .../rules/js-tosorted-immutable.md | 57 + .../rules/rendering-activity.md | 26 + .../rules/rendering-animate-svg-wrapper.md | 38 + .../rules/rendering-conditional-render.md | 32 + .../rules/rendering-content-visibility.md | 38 + .../rules/rendering-hoist-jsx.md | 36 + .../rules/rendering-hydration-no-flicker.md | 72 + .../rules/rendering-svg-precision.md | 28 + .../rules/rendering-usetransition-loading.md | 75 + .../rules/rerender-defer-reads.md | 39 + .../rules/rerender-dependencies.md | 45 + .../rules/rerender-derived-state.md | 29 + .../rules/rerender-functional-setstate.md | 77 + .../rules/rerender-lazy-state-init.md | 56 + .../rules/rerender-memo-with-default-value.md | 36 + .../rules/rerender-memo.md | 44 + .../rerender-simple-expression-in-memo.md | 35 + .../rules/rerender-transitions.md | 40 + .../rules/server-after-nonblocking.md | 73 + .../rules/server-auth-actions.md | 96 + .../rules/server-cache-lru.md | 41 + .../rules/server-cache-react.md | 76 + .../rules/server-dedup-props.md | 65 + .../rules/server-parallel-fetching.md | 83 + .../rules/server-serialization.md | 38 + 168 files changed, 18696 insertions(+) create mode 100644 .agent/skills/test-driven-development/SKILL.md create mode 100644 .agent/skills/test-driven-development/testing-anti-patterns.md create mode 100644 .agent/skills/vercel-react-best-practices/AGENTS.md create mode 100644 .agent/skills/vercel-react-best-practices/SKILL.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/advanced-use-latest.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/async-api-routes.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/async-defer-await.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/async-dependencies.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/async-parallel.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/bundle-conditional.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/bundle-preload.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/client-event-listeners.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/client-localstorage-schema.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/client-swr-dedup.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-batch-dom-css.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-cache-function-results.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-cache-property-access.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-cache-storage.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-combine-iterations.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-early-exit.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-hoist-regexp.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-index-maps.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-length-check-first.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-min-max-loop.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-set-map-lookups.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rendering-activity.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rendering-conditional-render.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rendering-content-visibility.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rendering-svg-precision.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rerender-defer-reads.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rerender-dependencies.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rerender-derived-state.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rerender-memo.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/rerender-transitions.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/server-after-nonblocking.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/server-auth-actions.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/server-cache-lru.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/server-cache-react.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/server-dedup-props.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/server-parallel-fetching.md create mode 100644 .agent/skills/vercel-react-best-practices/rules/server-serialization.md create mode 100644 .agents/skills/test-driven-development/SKILL.md create mode 100644 .agents/skills/test-driven-development/testing-anti-patterns.md create mode 100644 .agents/skills/vercel-react-best-practices/AGENTS.md create mode 100644 .agents/skills/vercel-react-best-practices/SKILL.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-api-routes.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-defer-await.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-dependencies.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-parallel.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-conditional.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-preload.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-event-listeners.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-storage.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-early-exit.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-index-maps.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-length-check-first.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-activity.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-memo.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-transitions.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-auth-actions.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-cache-lru.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-cache-react.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-dedup-props.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-serialization.md create mode 100644 .claude/skills/test-driven-development/SKILL.md create mode 100644 .claude/skills/test-driven-development/testing-anti-patterns.md create mode 100644 .claude/skills/vercel-react-best-practices/AGENTS.md create mode 100644 .claude/skills/vercel-react-best-practices/SKILL.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/async-api-routes.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/async-defer-await.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/async-dependencies.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/async-parallel.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/bundle-conditional.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/bundle-preload.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/client-event-listeners.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-cache-storage.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-early-exit.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-index-maps.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-length-check-first.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-activity.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-memo.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/rerender-transitions.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/server-auth-actions.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/server-cache-lru.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/server-cache-react.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/server-dedup-props.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md create mode 100644 .claude/skills/vercel-react-best-practices/rules/server-serialization.md diff --git a/.agent/skills/test-driven-development/SKILL.md b/.agent/skills/test-driven-development/SKILL.md new file mode 100644 index 00000000..33fb8512 --- /dev/null +++ b/.agent/skills/test-driven-development/SKILL.md @@ -0,0 +1,389 @@ +--- +name: test-driven-development +description: Use when implementing any feature or bugfix, before writing implementation code +--- + +# Test-Driven Development (TDD) + +## Overview + +Write the test first. Watch it fail. Write minimal code to pass. + +**Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing. + +**Violating the letter of the rules is violating the spirit of the rules.** + +## When to Use + +**Always:** + +- New features +- Bug fixes +- Refactoring +- Behavior changes + +**Exceptions (ask your human partner):** + +- Throwaway prototypes +- Generated code +- Configuration files + +Thinking "skip TDD just this once"? Stop. That's rationalization. + +## The Iron Law + +``` +NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST +``` + +Write code before the test? Delete it. Start over. + +**No exceptions:** + +- Don't keep it as "reference" +- Don't "adapt" it while writing tests +- Don't look at it +- Delete means delete + +Implement fresh from tests. Period. + +## Red-Green-Refactor + +```dot +digraph tdd_cycle { + rankdir=LR; + red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"]; + verify_red [label="Verify fails\ncorrectly", shape=diamond]; + green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"]; + verify_green [label="Verify passes\nAll green", shape=diamond]; + refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"]; + next [label="Next", shape=ellipse]; + + red -> verify_red; + verify_red -> green [label="yes"]; + verify_red -> red [label="wrong\nfailure"]; + green -> verify_green; + verify_green -> refactor [label="yes"]; + verify_green -> green [label="no"]; + refactor -> verify_green [label="stay\ngreen"]; + verify_green -> next; + next -> red; +} +``` + +### RED - Write Failing Test + +Write one minimal test showing what should happen. + + +```typescript +test('retries failed operations 3 times', async () => { + let attempts = 0; + const operation = () => { + attempts++; + if (attempts < 3) throw new Error('fail'); + return 'success'; + }; + +const result = await retryOperation(operation); + +expect(result).toBe('success'); +expect(attempts).toBe(3); +}); + +```` +Clear name, tests real behavior, one thing + + + +```typescript +test('retry works', async () => { + const mock = jest.fn() + .mockRejectedValueOnce(new Error()) + .mockRejectedValueOnce(new Error()) + .mockResolvedValueOnce('success'); + await retryOperation(mock); + expect(mock).toHaveBeenCalledTimes(3); +}); +```` + +Vague name, tests mock not code + + +**Requirements:** + +- One behavior +- Clear name +- Real code (no mocks unless unavoidable) + +### Verify RED - Watch It Fail + +**MANDATORY. Never skip.** + +```bash +npm test path/to/test.test.ts +``` + +Confirm: + +- Test fails (not errors) +- Failure message is expected +- Fails because feature missing (not typos) + +**Test passes?** You're testing existing behavior. Fix test. + +**Test errors?** Fix error, re-run until it fails correctly. + +### GREEN - Minimal Code + +Write simplest code to pass the test. + + +```typescript +async function retryOperation(fn: () => Promise): Promise { + for (let i = 0; i < 3; i++) { + try { + return await fn(); + } catch (e) { + if (i === 2) throw e; + } + } + throw new Error('unreachable'); +} +``` +Just enough to pass + + + +```typescript +async function retryOperation( + fn: () => Promise, + options?: { + maxRetries?: number; + backoff?: 'linear' | 'exponential'; + onRetry?: (attempt: number) => void; + } +): Promise { + // YAGNI +} +``` +Over-engineered + + +Don't add features, refactor other code, or "improve" beyond the test. + +### Verify GREEN - Watch It Pass + +**MANDATORY.** + +```bash +npm test path/to/test.test.ts +``` + +Confirm: + +- Test passes +- Other tests still pass +- Output pristine (no errors, warnings) + +**Test fails?** Fix code, not test. + +**Other tests fail?** Fix now. + +### REFACTOR - Clean Up + +After green only: + +- Remove duplication +- Improve names +- Extract helpers + +Keep tests green. Don't add behavior. + +### Repeat + +Next failing test for next feature. + +## Good Tests + +| Quality | Good | Bad | +| ---------------- | ----------------------------------- | --------------------------------------------------- | +| **Minimal** | One thing. "and" in name? Split it. | `test('validates email and domain and whitespace')` | +| **Clear** | Name describes behavior | `test('test1')` | +| **Shows intent** | Demonstrates desired API | Obscures what code should do | + +## Why Order Matters + +**"I'll write tests after to verify it works"** + +Tests written after code pass immediately. Passing immediately proves nothing: + +- Might test wrong thing +- Might test implementation, not behavior +- Might miss edge cases you forgot +- You never saw it catch the bug + +Test-first forces you to see the test fail, proving it actually tests something. + +**"I already manually tested all the edge cases"** + +Manual testing is ad-hoc. You think you tested everything but: + +- No record of what you tested +- Can't re-run when code changes +- Easy to forget cases under pressure +- "It worked when I tried it" ≠ comprehensive + +Automated tests are systematic. They run the same way every time. + +**"Deleting X hours of work is wasteful"** + +Sunk cost fallacy. The time is already gone. Your choice now: + +- Delete and rewrite with TDD (X more hours, high confidence) +- Keep it and add tests after (30 min, low confidence, likely bugs) + +The "waste" is keeping code you can't trust. Working code without real tests is technical debt. + +**"TDD is dogmatic, being pragmatic means adapting"** + +TDD IS pragmatic: + +- Finds bugs before commit (faster than debugging after) +- Prevents regressions (tests catch breaks immediately) +- Documents behavior (tests show how to use code) +- Enables refactoring (change freely, tests catch breaks) + +"Pragmatic" shortcuts = debugging in production = slower. + +**"Tests after achieve the same goals - it's spirit not ritual"** + +No. Tests-after answer "What does this do?" Tests-first answer "What should this do?" + +Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones. + +Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't). + +30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work. + +## Common Rationalizations + +| Excuse | Reality | +| -------------------------------------- | ----------------------------------------------------------------------- | +| "Too simple to test" | Simple code breaks. Test takes 30 seconds. | +| "I'll test after" | Tests passing immediately prove nothing. | +| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" | +| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. | +| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. | +| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. | +| "Need to explore first" | Fine. Throw away exploration, start with TDD. | +| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. | +| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. | +| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. | +| "Existing code has no tests" | You're improving it. Add tests for existing code. | + +## Red Flags - STOP and Start Over + +- Code before test +- Test after implementation +- Test passes immediately +- Can't explain why test failed +- Tests added "later" +- Rationalizing "just this once" +- "I already manually tested it" +- "Tests after achieve the same purpose" +- "It's about spirit not ritual" +- "Keep as reference" or "adapt existing code" +- "Already spent X hours, deleting is wasteful" +- "TDD is dogmatic, I'm being pragmatic" +- "This is different because..." + +**All of these mean: Delete code. Start over with TDD.** + +## Example: Bug Fix + +**Bug:** Empty email accepted + +**RED** + +```typescript +test('rejects empty email', async () => { + const result = await submitForm({ email: '' }); + expect(result.error).toBe('Email required'); +}); +``` + +**Verify RED** + +```bash +$ npm test +FAIL: expected 'Email required', got undefined +``` + +**GREEN** + +```typescript +function submitForm(data: FormData) { + if (!data.email?.trim()) { + return { error: 'Email required' }; + } + // ... +} +``` + +**Verify GREEN** + +```bash +$ npm test +PASS +``` + +**REFACTOR** +Extract validation for multiple fields if needed. + +## Verification Checklist + +Before marking work complete: + +- [ ] Every new function/method has a test +- [ ] Watched each test fail before implementing +- [ ] Each test failed for expected reason (feature missing, not typo) +- [ ] Wrote minimal code to pass each test +- [ ] All tests pass +- [ ] Output pristine (no errors, warnings) +- [ ] Tests use real code (mocks only if unavoidable) +- [ ] Edge cases and errors covered + +Can't check all boxes? You skipped TDD. Start over. + +## When Stuck + +| Problem | Solution | +| ---------------------- | -------------------------------------------------------------------- | +| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. | +| Test too complicated | Design too complicated. Simplify interface. | +| Must mock everything | Code too coupled. Use dependency injection. | +| Test setup huge | Extract helpers. Still complex? Simplify design. | + +## Debugging Integration + +Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression. + +Never fix bugs without a test. + +## Testing Anti-Patterns + +When adding mocks or test utilities, read @testing-anti-patterns.md to avoid common pitfalls: + +- Testing mock behavior instead of real behavior +- Adding test-only methods to production classes +- Mocking without understanding dependencies + +## Final Rule + +``` +Production code → test exists and failed first +Otherwise → not TDD +``` + +No exceptions without your human partner's permission. diff --git a/.agent/skills/test-driven-development/testing-anti-patterns.md b/.agent/skills/test-driven-development/testing-anti-patterns.md new file mode 100644 index 00000000..d0a654fa --- /dev/null +++ b/.agent/skills/test-driven-development/testing-anti-patterns.md @@ -0,0 +1,317 @@ +# Testing Anti-Patterns + +**Load this reference when:** writing or changing tests, adding mocks, or tempted to add test-only methods to production code. + +## Overview + +Tests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested. + +**Core principle:** Test what the code does, not what the mocks do. + +**Following strict TDD prevents these anti-patterns.** + +## The Iron Laws + +``` +1. NEVER test mock behavior +2. NEVER add test-only methods to production classes +3. NEVER mock without understanding dependencies +``` + +## Anti-Pattern 1: Testing Mock Behavior + +**The violation:** + +```typescript +// ❌ BAD: Testing that the mock exists +test('renders sidebar', () => { + render(); + expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument(); +}); +``` + +**Why this is wrong:** + +- You're verifying the mock works, not that the component works +- Test passes when mock is present, fails when it's not +- Tells you nothing about real behavior + +**your human partner's correction:** "Are we testing the behavior of a mock?" + +**The fix:** + +```typescript +// ✅ GOOD: Test real component or don't mock it +test('renders sidebar', () => { + render(); // Don't mock sidebar + expect(screen.getByRole('navigation')).toBeInTheDocument(); +}); + +// OR if sidebar must be mocked for isolation: +// Don't assert on the mock - test Page's behavior with sidebar present +``` + +### Gate Function + +``` +BEFORE asserting on any mock element: + Ask: "Am I testing real component behavior or just mock existence?" + + IF testing mock existence: + STOP - Delete the assertion or unmock the component + + Test real behavior instead +``` + +## Anti-Pattern 2: Test-Only Methods in Production + +**The violation:** + +```typescript +// ❌ BAD: destroy() only used in tests +class Session { + async destroy() { + // Looks like production API! + await this._workspaceManager?.destroyWorkspace(this.id); + // ... cleanup + } +} + +// In tests +afterEach(() => session.destroy()); +``` + +**Why this is wrong:** + +- Production class polluted with test-only code +- Dangerous if accidentally called in production +- Violates YAGNI and separation of concerns +- Confuses object lifecycle with entity lifecycle + +**The fix:** + +```typescript +// ✅ GOOD: Test utilities handle test cleanup +// Session has no destroy() - it's stateless in production + +// In test-utils/ +export async function cleanupSession(session: Session) { + const workspace = session.getWorkspaceInfo(); + if (workspace) { + await workspaceManager.destroyWorkspace(workspace.id); + } +} + +// In tests +afterEach(() => cleanupSession(session)); +``` + +### Gate Function + +``` +BEFORE adding any method to production class: + Ask: "Is this only used by tests?" + + IF yes: + STOP - Don't add it + Put it in test utilities instead + + Ask: "Does this class own this resource's lifecycle?" + + IF no: + STOP - Wrong class for this method +``` + +## Anti-Pattern 3: Mocking Without Understanding + +**The violation:** + +```typescript +// ❌ BAD: Mock breaks test logic +test('detects duplicate server', () => { + // Mock prevents config write that test depends on! + vi.mock('ToolCatalog', () => ({ + discoverAndCacheTools: vi.fn().mockResolvedValue(undefined), + })); + + await addServer(config); + await addServer(config); // Should throw - but won't! +}); +``` + +**Why this is wrong:** + +- Mocked method had side effect test depended on (writing config) +- Over-mocking to "be safe" breaks actual behavior +- Test passes for wrong reason or fails mysteriously + +**The fix:** + +```typescript +// ✅ GOOD: Mock at correct level +test('detects duplicate server', () => { + // Mock the slow part, preserve behavior test needs + vi.mock('MCPServerManager'); // Just mock slow server startup + + await addServer(config); // Config written + await addServer(config); // Duplicate detected ✓ +}); +``` + +### Gate Function + +``` +BEFORE mocking any method: + STOP - Don't mock yet + + 1. Ask: "What side effects does the real method have?" + 2. Ask: "Does this test depend on any of those side effects?" + 3. Ask: "Do I fully understand what this test needs?" + + IF depends on side effects: + Mock at lower level (the actual slow/external operation) + OR use test doubles that preserve necessary behavior + NOT the high-level method the test depends on + + IF unsure what test depends on: + Run test with real implementation FIRST + Observe what actually needs to happen + THEN add minimal mocking at the right level + + Red flags: + - "I'll mock this to be safe" + - "This might be slow, better mock it" + - Mocking without understanding the dependency chain +``` + +## Anti-Pattern 4: Incomplete Mocks + +**The violation:** + +```typescript +// ❌ BAD: Partial mock - only fields you think you need +const mockResponse = { + status: 'success', + data: { userId: '123', name: 'Alice' }, + // Missing: metadata that downstream code uses +}; + +// Later: breaks when code accesses response.metadata.requestId +``` + +**Why this is wrong:** + +- **Partial mocks hide structural assumptions** - You only mocked fields you know about +- **Downstream code may depend on fields you didn't include** - Silent failures +- **Tests pass but integration fails** - Mock incomplete, real API complete +- **False confidence** - Test proves nothing about real behavior + +**The Iron Rule:** Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses. + +**The fix:** + +```typescript +// ✅ GOOD: Mirror real API completeness +const mockResponse = { + status: 'success', + data: { userId: '123', name: 'Alice' }, + metadata: { requestId: 'req-789', timestamp: 1234567890 }, + // All fields real API returns +}; +``` + +### Gate Function + +``` +BEFORE creating mock responses: + Check: "What fields does the real API response contain?" + + Actions: + 1. Examine actual API response from docs/examples + 2. Include ALL fields system might consume downstream + 3. Verify mock matches real response schema completely + + Critical: + If you're creating a mock, you must understand the ENTIRE structure + Partial mocks fail silently when code depends on omitted fields + + If uncertain: Include all documented fields +``` + +## Anti-Pattern 5: Integration Tests as Afterthought + +**The violation:** + +``` +✅ Implementation complete +❌ No tests written +"Ready for testing" +``` + +**Why this is wrong:** + +- Testing is part of implementation, not optional follow-up +- TDD would have caught this +- Can't claim complete without tests + +**The fix:** + +``` +TDD cycle: +1. Write failing test +2. Implement to pass +3. Refactor +4. THEN claim complete +``` + +## When Mocks Become Too Complex + +**Warning signs:** + +- Mock setup longer than test logic +- Mocking everything to make test pass +- Mocks missing methods real components have +- Test breaks when mock changes + +**your human partner's question:** "Do we need to be using a mock here?" + +**Consider:** Integration tests with real components often simpler than complex mocks + +## TDD Prevents These Anti-Patterns + +**Why TDD helps:** + +1. **Write test first** → Forces you to think about what you're actually testing +2. **Watch it fail** → Confirms test tests real behavior, not mocks +3. **Minimal implementation** → No test-only methods creep in +4. **Real dependencies** → You see what the test actually needs before mocking + +**If you're testing mock behavior, you violated TDD** - you added mocks without watching test fail against real code first. + +## Quick Reference + +| Anti-Pattern | Fix | +| ------------------------------- | --------------------------------------------- | +| Assert on mock elements | Test real component or unmock it | +| Test-only methods in production | Move to test utilities | +| Mock without understanding | Understand dependencies first, mock minimally | +| Incomplete mocks | Mirror real API completely | +| Tests as afterthought | TDD - tests first | +| Over-complex mocks | Consider integration tests | + +## Red Flags + +- Assertion checks for `*-mock` test IDs +- Methods only called in test files +- Mock setup is >50% of test +- Test fails when you remove mock +- Can't explain why mock is needed +- Mocking "just to be safe" + +## The Bottom Line + +**Mocks are tools to isolate, not things to test.** + +If TDD reveals you're testing mock behavior, you've gone wrong. + +Fix: Test real behavior or question why you're mocking at all. diff --git a/.agent/skills/vercel-react-best-practices/AGENTS.md b/.agent/skills/vercel-react-best-practices/AGENTS.md new file mode 100644 index 00000000..ef35f360 --- /dev/null +++ b/.agent/skills/vercel-react-best-practices/AGENTS.md @@ -0,0 +1,2666 @@ +# React Best Practices + +**Version 1.0.0** +Vercel Engineering +January 2026 + +> **Note:** +> This document is mainly for agents and LLMs to follow when maintaining, +> generating, or refactoring React and Next.js codebases at Vercel. Humans +> may also find it useful, but guidance here is optimized for automation +> and consistency by AI-assisted workflows. + +--- + +## Abstract + +Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation. + +--- + +## Table of Contents + +1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL** + - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed) + - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization) + - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes) + - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations) + - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries) +2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL** + - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports) + - 2.2 [Conditional Module Loading](#22-conditional-module-loading) + - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries) + - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components) + - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent) +3. [Server-Side Performance](#3-server-side-performance) — **HIGH** + - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes) + - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props) + - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching) + - 3.4 [Minimize Serialization at RSC Boundaries](#34-minimize-serialization-at-rsc-boundaries) + - 3.5 [Parallel Data Fetching with Component Composition](#35-parallel-data-fetching-with-component-composition) + - 3.6 [Per-Request Deduplication with React.cache()](#36-per-request-deduplication-with-reactcache) + - 3.7 [Use after() for Non-Blocking Operations](#37-use-after-for-non-blocking-operations) +4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH** + - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners) + - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance) + - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication) + - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data) +5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM** + - 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point) + - 5.2 [Do not wrap a simple expression with a primitive result type in useMemo](#52-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo) + - 5.3 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#53-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant) + - 5.4 [Extract to Memoized Components](#54-extract-to-memoized-components) + - 5.5 [Narrow Effect Dependencies](#55-narrow-effect-dependencies) + - 5.6 [Subscribe to Derived State](#56-subscribe-to-derived-state) + - 5.7 [Use Functional setState Updates](#57-use-functional-setstate-updates) + - 5.8 [Use Lazy State Initialization](#58-use-lazy-state-initialization) + - 5.9 [Use Transitions for Non-Urgent Updates](#59-use-transitions-for-non-urgent-updates) +6. [Rendering Performance](#6-rendering-performance) — **MEDIUM** + - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element) + - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists) + - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements) + - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision) + - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering) + - 6.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide) + - 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering) + - 6.8 [Use useTransition Over Manual Loading States](#68-use-usetransition-over-manual-loading-states) +7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM** + - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing) + - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups) + - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops) + - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls) + - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls) + - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations) + - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons) + - 7.8 [Early Return from Functions](#78-early-return-from-functions) + - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation) + - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort) + - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups) + - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability) +8. [Advanced Patterns](#8-advanced-patterns) — **LOW** + - 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs) + - 8.2 [useEffectEvent for Stable Callback Refs](#82-useeffectevent-for-stable-callback-refs) + +--- + +## 1. Eliminating Waterfalls + +**Impact: CRITICAL** + +Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains. + +### 1.1 Defer Await Until Needed + +**Impact: HIGH (avoids blocking unused code paths)** + +Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them. + +**Incorrect: blocks both branches** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + const userData = await fetchUserData(userId); + + if (skipProcessing) { + // Returns immediately but still waited for userData + return { skipped: true }; + } + + // Only this branch uses userData + return processUserData(userData); +} +``` + +**Correct: only blocks when needed** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + if (skipProcessing) { + // Returns immediately without waiting + return { skipped: true }; + } + + // Fetch only when needed + const userData = await fetchUserData(userId); + return processUserData(userData); +} +``` + +**Another example: early return optimization** + +```typescript +// Incorrect: always fetches permissions +async function updateResource(resourceId: string, userId: string) { + const permissions = await fetchPermissions(userId); + const resource = await getResource(resourceId); + + if (!resource) { + return { error: 'Not found' }; + } + + if (!permissions.canEdit) { + return { error: 'Forbidden' }; + } + + return await updateResourceData(resource, permissions); +} + +// Correct: fetches only when needed +async function updateResource(resourceId: string, userId: string) { + const resource = await getResource(resourceId); + + if (!resource) { + return { error: 'Not found' }; + } + + const permissions = await fetchPermissions(userId); + + if (!permissions.canEdit) { + return { error: 'Forbidden' }; + } + + return await updateResourceData(resource, permissions); +} +``` + +This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive. + +### 1.2 Dependency-Based Parallelization + +**Impact: CRITICAL (2-10× improvement)** + +For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment. + +**Incorrect: profile waits for config unnecessarily** + +```typescript +const [user, config] = await Promise.all([fetchUser(), fetchConfig()]); +const profile = await fetchProfile(user.id); +``` + +**Correct: config and profile run in parallel** + +```typescript +import { all } from 'better-all'; + +const { user, config, profile } = await all({ + async user() { + return fetchUser(); + }, + async config() { + return fetchConfig(); + }, + async profile() { + return fetchProfile((await this.$.user).id); + }, +}); +``` + +**Alternative without extra dependencies:** + +```typescript +const userPromise = fetchUser(); +const profilePromise = userPromise.then((user) => fetchProfile(user.id)); + +const [user, config, profile] = await Promise.all([userPromise, fetchConfig(), profilePromise]); +``` + +We can also create all the promises first, and do `Promise.all()` at the end. + +Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) + +### 1.3 Prevent Waterfall Chains in API Routes + +**Impact: CRITICAL (2-10× improvement)** + +In API routes and Server Actions, start independent operations immediately, even if you don't await them yet. + +**Incorrect: config waits for auth, data waits for both** + +```typescript +export async function GET(request: Request) { + const session = await auth(); + const config = await fetchConfig(); + const data = await fetchData(session.user.id); + return Response.json({ data, config }); +} +``` + +**Correct: auth and config start immediately** + +```typescript +export async function GET(request: Request) { + const sessionPromise = auth(); + const configPromise = fetchConfig(); + const session = await sessionPromise; + const [config, data] = await Promise.all([configPromise, fetchData(session.user.id)]); + return Response.json({ data, config }); +} +``` + +For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization). + +### 1.4 Promise.all() for Independent Operations + +**Impact: CRITICAL (2-10× improvement)** + +When async operations have no interdependencies, execute them concurrently using `Promise.all()`. + +**Incorrect: sequential execution, 3 round trips** + +```typescript +const user = await fetchUser(); +const posts = await fetchPosts(); +const comments = await fetchComments(); +``` + +**Correct: parallel execution, 1 round trip** + +```typescript +const [user, posts, comments] = await Promise.all([fetchUser(), fetchPosts(), fetchComments()]); +``` + +### 1.5 Strategic Suspense Boundaries + +**Impact: HIGH (faster initial paint)** + +Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads. + +**Incorrect: wrapper blocked by data fetching** + +```tsx +async function Page() { + const data = await fetchData(); // Blocks entire page + + return ( +
+
Sidebar
+
Header
+
+ +
+
Footer
+
+ ); +} +``` + +The entire layout waits for data even though only the middle section needs it. + +**Correct: wrapper shows immediately, data streams in** + +```tsx +function Page() { + return ( +
+
Sidebar
+
Header
+
+ }> + + +
+
Footer
+
+ ); +} + +async function DataDisplay() { + const data = await fetchData(); // Only blocks this component + return
{data.content}
; +} +``` + +Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. + +**Alternative: share promise across components** + +```tsx +function Page() { + // Start fetch immediately, but don't await + const dataPromise = fetchData(); + + return ( +
+
Sidebar
+
Header
+ }> + + + +
Footer
+
+ ); +} + +function DataDisplay({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise); // Unwraps the promise + return
{data.content}
; +} + +function DataSummary({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise); // Reuses the same promise + return
{data.summary}
; +} +``` + +Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together. + +**When NOT to use this pattern:** + +- Critical data needed for layout decisions (affects positioning) + +- SEO-critical content above the fold + +- Small, fast queries where suspense overhead isn't worth it + +- When you want to avoid layout shift (loading → content jump) + +**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities. + +--- + +## 2. Bundle Size Optimization + +**Impact: CRITICAL** + +Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. + +### 2.1 Avoid Barrel File Imports + +**Impact: CRITICAL (200-800ms import cost, slow builds)** + +Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`). + +Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts. + +**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph. + +**Incorrect: imports entire library** + +```tsx +import { Check, X, Menu } from 'lucide-react'; +// Loads 1,583 modules, takes ~2.8s extra in dev +// Runtime cost: 200-800ms on every cold start + +import { Button, TextField } from '@mui/material'; +// Loads 2,225 modules, takes ~4.2s extra in dev +``` + +**Correct: imports only what you need** + +```tsx +import Check from 'lucide-react/dist/esm/icons/check'; +import X from 'lucide-react/dist/esm/icons/x'; +import Menu from 'lucide-react/dist/esm/icons/menu'; +// Loads only 3 modules (~2KB vs ~1MB) + +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +// Loads only what you use +``` + +**Alternative: Next.js 13.5+** + +```js +// next.config.js - use optimizePackageImports +module.exports = { + experimental: { + optimizePackageImports: ['lucide-react', '@mui/material'], + }, +}; + +// Then you can keep the ergonomic barrel imports: +import { Check, X, Menu } from 'lucide-react'; +// Automatically transformed to direct imports at build time +``` + +Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR. + +Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`. + +Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) + +### 2.2 Conditional Module Loading + +**Impact: HIGH (loads large data only when needed)** + +Load large data or modules only when a feature is activated. + +**Example: lazy-load animation frames** + +```tsx +function AnimationPlayer({ + enabled, + setEnabled, +}: { + enabled: boolean; + setEnabled: React.Dispatch>; +}) { + const [frames, setFrames] = useState(null); + + useEffect(() => { + if (enabled && !frames && typeof window !== 'undefined') { + import('./animation-frames.js').then((mod) => setFrames(mod.frames)).catch(() => setEnabled(false)); + } + }, [enabled, frames, setEnabled]); + + if (!frames) return ; + return ; +} +``` + +The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed. + +### 2.3 Defer Non-Critical Third-Party Libraries + +**Impact: MEDIUM (loads after hydration)** + +Analytics, logging, and error tracking don't block user interaction. Load them after hydration. + +**Incorrect: blocks initial bundle** + +```tsx +import { Analytics } from '@vercel/analytics/react'; + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ); +} +``` + +**Correct: loads after hydration** + +```tsx +import dynamic from 'next/dynamic'; + +const Analytics = dynamic(() => import('@vercel/analytics/react').then((m) => m.Analytics), { ssr: false }); + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ); +} +``` + +### 2.4 Dynamic Imports for Heavy Components + +**Impact: CRITICAL (directly affects TTI and LCP)** + +Use `next/dynamic` to lazy-load large components not needed on initial render. + +**Incorrect: Monaco bundles with main chunk ~300KB** + +```tsx +import { MonacoEditor } from './monaco-editor'; + +function CodePanel({ code }: { code: string }) { + return ; +} +``` + +**Correct: Monaco loads on demand** + +```tsx +import dynamic from 'next/dynamic'; + +const MonacoEditor = dynamic(() => import('./monaco-editor').then((m) => m.MonacoEditor), { ssr: false }); + +function CodePanel({ code }: { code: string }) { + return ; +} +``` + +### 2.5 Preload Based on User Intent + +**Impact: MEDIUM (reduces perceived latency)** + +Preload heavy bundles before they're needed to reduce perceived latency. + +**Example: preload on hover/focus** + +```tsx +function EditorButton({ onClick }: { onClick: () => void }) { + const preload = () => { + if (typeof window !== 'undefined') { + void import('./monaco-editor'); + } + }; + + return ( + + ); +} +``` + +**Example: preload when feature flag is enabled** + +```tsx +function FlagsProvider({ children, flags }: Props) { + useEffect(() => { + if (flags.editorEnabled && typeof window !== 'undefined') { + void import('./monaco-editor').then((mod) => mod.init()); + } + }, [flags.editorEnabled]); + + return {children}; +} +``` + +The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed. + +--- + +## 3. Server-Side Performance + +**Impact: HIGH** + +Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. + +### 3.1 Authenticate Server Actions Like API Routes + +**Impact: CRITICAL (prevents unauthorized access to server mutations)** + +Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly. + +Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation." + +**Incorrect: no authentication check** + +```typescript +'use server'; + +export async function deleteUser(userId: string) { + // Anyone can call this! No auth check + await db.user.delete({ where: { id: userId } }); + return { success: true }; +} +``` + +**Correct: authentication inside the action** + +```typescript +'use server'; + +import { verifySession } from '@/lib/auth'; +import { unauthorized } from '@/lib/errors'; + +export async function deleteUser(userId: string) { + // Always check auth inside the action + const session = await verifySession(); + + if (!session) { + throw unauthorized('Must be logged in'); + } + + // Check authorization too + if (session.user.role !== 'admin' && session.user.id !== userId) { + throw unauthorized('Cannot delete other users'); + } + + await db.user.delete({ where: { id: userId } }); + return { success: true }; +} +``` + +**With input validation:** + +```typescript +'use server'; + +import { verifySession } from '@/lib/auth'; +import { z } from 'zod'; + +const updateProfileSchema = z.object({ + userId: z.string().uuid(), + name: z.string().min(1).max(100), + email: z.string().email(), +}); + +export async function updateProfile(data: unknown) { + // Validate input first + const validated = updateProfileSchema.parse(data); + + // Then authenticate + const session = await verifySession(); + if (!session) { + throw new Error('Unauthorized'); + } + + // Then authorize + if (session.user.id !== validated.userId) { + throw new Error('Can only update own profile'); + } + + // Finally perform the mutation + await db.user.update({ + where: { id: validated.userId }, + data: { + name: validated.name, + email: validated.email, + }, + }); + + return { success: true }; +} +``` + +Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication) + +### 3.2 Avoid Duplicate Serialization in RSC Props + +**Impact: LOW (reduces network payload by avoiding duplicate serialization)** + +RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server. + +**Incorrect: duplicates array** + +```tsx +// RSC: sends 6 strings (2 arrays × 3 items) + +``` + +**Correct: sends 3 strings** + +```tsx +// RSC: send once +; + +// Client: transform there +('use client'); +const sorted = useMemo(() => [...usernames].sort(), [usernames]); +``` + +**Nested deduplication behavior:** + +```tsx +// string[] - duplicates everything +usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings + +// object[] - duplicates array structure only +users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4) +``` + +Deduplication works recursively. Impact varies by data type: + +- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated + +- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference + +**Operations breaking deduplication: create new references** + +- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]` + +- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())` + +**More examples:** + +```tsx +// ❌ Bad + u.active)} /> + + +// ✅ Good + + +// Do filtering/destructuring in client +``` + +**Exception:** Pass derived data when transformation is expensive or client doesn't need original. + +### 3.3 Cross-Request LRU Caching + +**Impact: HIGH (caches across requests)** + +`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache. + +**Implementation:** + +```typescript +import { LRUCache } from 'lru-cache'; + +const cache = new LRUCache({ + max: 1000, + ttl: 5 * 60 * 1000, // 5 minutes +}); + +export async function getUser(id: string) { + const cached = cache.get(id); + if (cached) return cached; + + const user = await db.user.findUnique({ where: { id } }); + cache.set(id, user); + return user; +} + +// Request 1: DB query, result cached +// Request 2: cache hit, no DB query +``` + +Use when sequential user actions hit multiple endpoints needing the same data within seconds. + +**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis. + +**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching. + +Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache) + +### 3.4 Minimize Serialization at RSC Boundaries + +**Impact: HIGH (reduces data transfer size)** + +The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses. + +**Incorrect: serializes all 50 fields** + +```tsx +async function Page() { + const user = await fetchUser(); // 50 fields + return ; +} + +('use client'); +function Profile({ user }: { user: User }) { + return
{user.name}
; // uses 1 field +} +``` + +**Correct: serializes only 1 field** + +```tsx +async function Page() { + const user = await fetchUser(); + return ; +} + +('use client'); +function Profile({ name }: { name: string }) { + return
{name}
; +} +``` + +### 3.5 Parallel Data Fetching with Component Composition + +**Impact: CRITICAL (eliminates server-side waterfalls)** + +React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching. + +**Incorrect: Sidebar waits for Page's fetch to complete** + +```tsx +export default async function Page() { + const header = await fetchHeader(); + return ( +
+
{header}
+ +
+ ); +} + +async function Sidebar() { + const items = await fetchSidebarItems(); + return ; +} +``` + +**Correct: both fetch simultaneously** + +```tsx +async function Header() { + const data = await fetchHeader(); + return
{data}
; +} + +async function Sidebar() { + const items = await fetchSidebarItems(); + return ; +} + +export default function Page() { + return ( +
+
+ +
+ ); +} +``` + +**Alternative with children prop:** + +```tsx +async function Header() { + const data = await fetchHeader(); + return
{data}
; +} + +async function Sidebar() { + const items = await fetchSidebarItems(); + return ; +} + +function Layout({ children }: { children: ReactNode }) { + return ( +
+
+ {children} +
+ ); +} + +export default function Page() { + return ( + + + + ); +} +``` + +### 3.6 Per-Request Deduplication with React.cache() + +**Impact: MEDIUM (deduplicates within request)** + +Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most. + +**Usage:** + +```typescript +import { cache } from 'react'; + +export const getCurrentUser = cache(async () => { + const session = await auth(); + if (!session?.user?.id) return null; + return await db.user.findUnique({ + where: { id: session.user.id }, + }); +}); +``` + +Within a single request, multiple calls to `getCurrentUser()` execute the query only once. + +**Avoid inline objects as arguments:** + +`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits. + +**Incorrect: always cache miss** + +```typescript +const getUser = cache(async (params: { uid: number }) => { + return await db.user.findUnique({ where: { id: params.uid } }); +}); + +// Each call creates new object, never hits cache +getUser({ uid: 1 }); +getUser({ uid: 1 }); // Cache miss, runs query again +``` + +**Correct: cache hit** + +```typescript +const params = { uid: 1 }; +getUser(params); // Query runs +getUser(params); // Cache hit (same reference) +``` + +If you must pass objects, pass the same reference: + +**Next.js-Specific Note:** + +In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks: + +- Database queries (Prisma, Drizzle, etc.) + +- Heavy computations + +- Authentication checks + +- File system operations + +- Any non-fetch async work + +Use `React.cache()` to deduplicate these operations across your component tree. + +Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache) + +### 3.7 Use after() for Non-Blocking Operations + +**Impact: MEDIUM (faster response times)** + +Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response. + +**Incorrect: blocks response** + +```tsx +import { logUserAction } from '@/app/utils'; + +export async function POST(request: Request) { + // Perform mutation + await updateDatabase(request); + + // Logging blocks the response + const userAgent = request.headers.get('user-agent') || 'unknown'; + await logUserAction({ userAgent }); + + return new Response(JSON.stringify({ status: 'success' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); +} +``` + +**Correct: non-blocking** + +```tsx +import { after } from 'next/server'; +import { headers, cookies } from 'next/headers'; +import { logUserAction } from '@/app/utils'; + +export async function POST(request: Request) { + // Perform mutation + await updateDatabase(request); + + // Log after response is sent + after(async () => { + const userAgent = (await headers()).get('user-agent') || 'unknown'; + const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'; + + logUserAction({ sessionCookie, userAgent }); + }); + + return new Response(JSON.stringify({ status: 'success' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); +} +``` + +The response is sent immediately while logging happens in the background. + +**Common use cases:** + +- Analytics tracking + +- Audit logging + +- Sending notifications + +- Cache invalidation + +- Cleanup tasks + +**Important notes:** + +- `after()` runs even if the response fails or redirects + +- Works in Server Actions, Route Handlers, and Server Components + +Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after) + +--- + +## 4. Client-Side Data Fetching + +**Impact: MEDIUM-HIGH** + +Automatic deduplication and efficient data fetching patterns reduce redundant network requests. + +### 4.1 Deduplicate Global Event Listeners + +**Impact: LOW (single listener for N components)** + +Use `useSWRSubscription()` to share global event listeners across component instances. + +**Incorrect: N instances = N listeners** + +```tsx +function useKeyboardShortcut(key: string, callback: () => void) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && e.key === key) { + callback(); + } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [key, callback]); +} +``` + +When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener. + +**Correct: N instances = 1 listener** + +```tsx +import useSWRSubscription from 'swr/subscription'; + +// Module-level Map to track callbacks per key +const keyCallbacks = new Map void>>(); + +function useKeyboardShortcut(key: string, callback: () => void) { + // Register this callback in the Map + useEffect(() => { + if (!keyCallbacks.has(key)) { + keyCallbacks.set(key, new Set()); + } + keyCallbacks.get(key)!.add(callback); + + return () => { + const set = keyCallbacks.get(key); + if (set) { + set.delete(callback); + if (set.size === 0) { + keyCallbacks.delete(key); + } + } + }; + }, [key, callback]); + + useSWRSubscription('global-keydown', () => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && keyCallbacks.has(e.key)) { + keyCallbacks.get(e.key)!.forEach((cb) => cb()); + } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }); +} + +function Profile() { + // Multiple shortcuts will share the same listener + useKeyboardShortcut('p', () => { + /* ... */ + }); + useKeyboardShortcut('k', () => { + /* ... */ + }); + // ... +} +``` + +### 4.2 Use Passive Event Listeners for Scrolling Performance + +**Impact: MEDIUM (eliminates scroll delay caused by event listeners)** + +Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay. + +**Incorrect:** + +```typescript +useEffect(() => { + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX); + const handleWheel = (e: WheelEvent) => console.log(e.deltaY); + + document.addEventListener('touchstart', handleTouch); + document.addEventListener('wheel', handleWheel); + + return () => { + document.removeEventListener('touchstart', handleTouch); + document.removeEventListener('wheel', handleWheel); + }; +}, []); +``` + +**Correct:** + +```typescript +useEffect(() => { + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX); + const handleWheel = (e: WheelEvent) => console.log(e.deltaY); + + document.addEventListener('touchstart', handleTouch, { passive: true }); + document.addEventListener('wheel', handleWheel, { passive: true }); + + return () => { + document.removeEventListener('touchstart', handleTouch); + document.removeEventListener('wheel', handleWheel); + }; +}, []); +``` + +**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`. + +**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`. + +### 4.3 Use SWR for Automatic Deduplication + +**Impact: MEDIUM-HIGH (automatic deduplication)** + +SWR enables request deduplication, caching, and revalidation across component instances. + +**Incorrect: no deduplication, each instance fetches** + +```tsx +function UserList() { + const [users, setUsers] = useState([]); + useEffect(() => { + fetch('/api/users') + .then((r) => r.json()) + .then(setUsers); + }, []); +} +``` + +**Correct: multiple instances share one request** + +```tsx +import useSWR from 'swr'; + +function UserList() { + const { data: users } = useSWR('/api/users', fetcher); +} +``` + +**For immutable data:** + +```tsx +import { useImmutableSWR } from '@/lib/swr'; + +function StaticContent() { + const { data } = useImmutableSWR('/api/config', fetcher); +} +``` + +**For mutations:** + +```tsx +import { useSWRMutation } from 'swr/mutation'; + +function UpdateButton() { + const { trigger } = useSWRMutation('/api/user', updateUser); + return ; +} +``` + +Reference: [https://swr.vercel.app](https://swr.vercel.app) + +### 4.4 Version and Minimize localStorage Data + +**Impact: MEDIUM (prevents schema conflicts, reduces storage size)** + +Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data. + +**Incorrect:** + +```typescript +// No version, stores everything, no error handling +localStorage.setItem('userConfig', JSON.stringify(fullUserObject)); +const data = localStorage.getItem('userConfig'); +``` + +**Correct:** + +```typescript +const VERSION = 'v2'; + +function saveConfig(config: { theme: string; language: string }) { + try { + localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config)); + } catch { + // Throws in incognito/private browsing, quota exceeded, or disabled + } +} + +function loadConfig() { + try { + const data = localStorage.getItem(`userConfig:${VERSION}`); + return data ? JSON.parse(data) : null; + } catch { + return null; + } +} + +// Migration from v1 to v2 +function migrate() { + try { + const v1 = localStorage.getItem('userConfig:v1'); + if (v1) { + const old = JSON.parse(v1); + saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang }); + localStorage.removeItem('userConfig:v1'); + } + } catch {} +} +``` + +**Store minimal fields from server responses:** + +```typescript +// User object has 20+ fields, only store what UI needs +function cachePrefs(user: FullUser) { + try { + localStorage.setItem( + 'prefs:v1', + JSON.stringify({ + theme: user.preferences.theme, + notifications: user.preferences.notifications, + }) + ); + } catch {} +} +``` + +**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled. + +**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags. + +--- + +## 5. Re-render Optimization + +**Impact: MEDIUM** + +Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. + +### 5.1 Defer State Reads to Usage Point + +**Impact: MEDIUM (avoids unnecessary subscriptions)** + +Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks. + +**Incorrect: subscribes to all searchParams changes** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const searchParams = useSearchParams(); + + const handleShare = () => { + const ref = searchParams.get('ref'); + shareChat(chatId, { ref }); + }; + + return ; +} +``` + +**Correct: reads on demand, no subscription** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const handleShare = () => { + const params = new URLSearchParams(window.location.search); + const ref = params.get('ref'); + shareChat(chatId, { ref }); + }; + + return ; +} +``` + +### 5.2 Do not wrap a simple expression with a primitive result type in useMemo + +**Impact: LOW-MEDIUM (wasted computation on every render)** + +When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`. + +Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself. + +**Incorrect:** + +```tsx +function Header({ user, notifications }: Props) { + const isLoading = useMemo(() => { + return user.isLoading || notifications.isLoading; + }, [user.isLoading, notifications.isLoading]); + + if (isLoading) return ; + // return some markup +} +``` + +**Correct:** + +```tsx +function Header({ user, notifications }: Props) { + const isLoading = user.isLoading || notifications.isLoading; + + if (isLoading) return ; + // return some markup +} +``` + +### 5.3 Extract Default Non-primitive Parameter Value from Memoized Component to Constant + +**Impact: MEDIUM (restores memoization by using a constant for default value)** + +When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`. + +To address this issue, extract the default value into a constant. + +**Incorrect: `onClick` has different values on every rerender** + +```tsx +const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) { + // ... +}) + +// Used without optional onClick + +``` + +**Correct: stable default value** + +```tsx +const NOOP = () => {}; + +const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) { + // ... +}) + +// Used without optional onClick + +``` + +### 5.4 Extract to Memoized Components + +**Impact: MEDIUM (enables early returns)** + +Extract expensive work into memoized components to enable early returns before computation. + +**Incorrect: computes avatar even when loading** + +```tsx +function Profile({ user, loading }: Props) { + const avatar = useMemo(() => { + const id = computeAvatarId(user); + return ; + }, [user]); + + if (loading) return ; + return
{avatar}
; +} +``` + +**Correct: skips computation when loading** + +```tsx +const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { + const id = useMemo(() => computeAvatarId(user), [user]); + return ; +}); + +function Profile({ user, loading }: Props) { + if (loading) return ; + return ( +
+ +
+ ); +} +``` + +**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders. + +### 5.5 Narrow Effect Dependencies + +**Impact: LOW (minimizes effect re-runs)** + +Specify primitive dependencies instead of objects to minimize effect re-runs. + +**Incorrect: re-runs on any user field change** + +```tsx +useEffect(() => { + console.log(user.id); +}, [user]); +``` + +**Correct: re-runs only when id changes** + +```tsx +useEffect(() => { + console.log(user.id); +}, [user.id]); +``` + +**For derived state, compute outside effect:** + +```tsx +// Incorrect: runs on width=767, 766, 765... +useEffect(() => { + if (width < 768) { + enableMobileMode(); + } +}, [width]); + +// Correct: runs only on boolean transition +const isMobile = width < 768; +useEffect(() => { + if (isMobile) { + enableMobileMode(); + } +}, [isMobile]); +``` + +### 5.6 Subscribe to Derived State + +**Impact: MEDIUM (reduces re-render frequency)** + +Subscribe to derived boolean state instead of continuous values to reduce re-render frequency. + +**Incorrect: re-renders on every pixel change** + +```tsx +function Sidebar() { + const width = useWindowWidth(); // updates continuously + const isMobile = width < 768; + return