mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
♻️ refactor(claude): migrate skills to new skill-creator format
Recreate all 11 skills using the updated skill-creator tooling: - api-sync, arch-docs, architecture-validator, css-animations - git-workflow, library-creator, logging, state-patterns - tailwind, template-standards, test-migration Key changes: - Updated YAML frontmatter structure - Reorganized logging references into references/ subdirectory - Applied progressive disclosure patterns where applicable
This commit is contained in:
@@ -1,371 +1,362 @@
|
|||||||
---
|
---
|
||||||
name: api-sync
|
name: api-sync
|
||||||
description: This skill should be used when regenerating Swagger/OpenAPI TypeScript API clients with breaking change detection. Handles generation of all 10 API clients (or specific ones), pre-generation impact analysis, Unicode cleanup, TypeScript validation, and affected test execution. Use when user requests "API sync", "regenerate swagger", "check breaking changes", or indicates backend API changes.
|
description: This skill should be used when regenerating Swagger/OpenAPI TypeScript API clients with breaking change detection. Handles generation of all 10 API clients (or specific ones), pre-generation impact analysis, Unicode cleanup, TypeScript validation, and affected test execution. Use when user requests "API sync", "regenerate swagger", "check breaking changes", or indicates backend API changes.
|
||||||
---
|
---
|
||||||
|
|
||||||
# API Sync Manager
|
# API Sync Manager
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Automate the complete lifecycle of TypeScript API client regeneration from Swagger/OpenAPI specifications. Provides pre-generation breaking change detection, automatic post-processing, impact analysis, validation, and migration recommendations for all 10 API clients in the ISA-Frontend monorepo.
|
Automate the complete lifecycle of TypeScript API client regeneration from Swagger/OpenAPI specifications. Provides pre-generation breaking change detection, automatic post-processing, impact analysis, validation, and migration recommendations for all 10 API clients in the ISA-Frontend monorepo.
|
||||||
|
|
||||||
## When to Use This Skill
|
## Available APIs
|
||||||
|
|
||||||
Invoke when user requests:
|
availability-api, cat-search-api, checkout-api, crm-api, eis-api, inventory-api, isa-api, oms-api, print-api, wws-api
|
||||||
- API client regeneration or sync
|
|
||||||
- "Check breaking changes" before API update
|
## Unified Sync Workflow
|
||||||
- Backend API changes need frontend updates
|
|
||||||
- Impact assessment of API changes
|
### Step 1: Pre-Generation Check
|
||||||
- "Regenerate swagger" or "update API clients"
|
|
||||||
|
```bash
|
||||||
## Available APIs
|
# Check uncommitted changes
|
||||||
|
git status generated/swagger/
|
||||||
availability-api, cat-search-api, checkout-api, crm-api, eis-api, inventory-api, isa-api, oms-api, print-api, wws-api
|
|
||||||
|
# Verify no manual edits will be lost
|
||||||
## Unified Sync Workflow
|
git diff generated/swagger/
|
||||||
|
```
|
||||||
### Step 1: Pre-Generation Check
|
|
||||||
|
If uncommitted changes exist, warn user and ask to proceed. If manual edits detected, strongly recommend committing first.
|
||||||
```bash
|
|
||||||
# Check uncommitted changes
|
### Step 2: Pre-Generation Breaking Change Detection
|
||||||
git status generated/swagger/
|
|
||||||
|
Generate to temporary location to compare without affecting working directory:
|
||||||
# Verify no manual edits will be lost
|
|
||||||
git diff generated/swagger/
|
```bash
|
||||||
```
|
# Backup current state
|
||||||
|
cp -r generated/swagger/[api-name] /tmp/[api-name].backup
|
||||||
If uncommitted changes exist, warn user and ask to proceed. If manual edits detected, strongly recommend committing first.
|
|
||||||
|
# Generate to temp location for comparison
|
||||||
### Step 2: Pre-Generation Breaking Change Detection
|
npm run generate:swagger:[api-name]
|
||||||
|
```
|
||||||
Generate to temporary location to compare without affecting working directory:
|
|
||||||
|
**Compare Models and Services:**
|
||||||
```bash
|
|
||||||
# Backup current state
|
```bash
|
||||||
cp -r generated/swagger/[api-name] /tmp/[api-name].backup
|
diff -u /tmp/[api-name].backup/models.ts generated/swagger/[api-name]/models.ts
|
||||||
|
diff -u /tmp/[api-name].backup/services.ts generated/swagger/[api-name]/services.ts
|
||||||
# Generate to temp location for comparison
|
```
|
||||||
npm run generate:swagger:[api-name]
|
|
||||||
```
|
**Categorize Changes:**
|
||||||
|
|
||||||
**Compare Models and Services:**
|
**🔴 Breaking (Critical):**
|
||||||
|
- Removed properties from response models
|
||||||
```bash
|
- Changed property types (string → number, object → array)
|
||||||
diff -u /tmp/[api-name].backup/models.ts generated/swagger/[api-name]/models.ts
|
- Removed endpoints
|
||||||
diff -u /tmp/[api-name].backup/services.ts generated/swagger/[api-name]/services.ts
|
- Optional → required fields in request models
|
||||||
```
|
- Removed enum values
|
||||||
|
- Changed endpoint paths or HTTP methods
|
||||||
**Categorize Changes:**
|
|
||||||
|
**⚠️ Warnings (Review Required):**
|
||||||
**🔴 Breaking (Critical):**
|
- Property renamed (old removed + new added)
|
||||||
- Removed properties from response models
|
- Changed default values
|
||||||
- Changed property types (string → number, object → array)
|
- Changed validation rules (min/max length, pattern)
|
||||||
- Removed endpoints
|
- Added required request fields
|
||||||
- Optional → required fields in request models
|
- Changed parameter locations (query → body)
|
||||||
- Removed enum values
|
|
||||||
- Changed endpoint paths or HTTP methods
|
**✅ Compatible (Safe):**
|
||||||
|
- Added properties to response models
|
||||||
**⚠️ Warnings (Review Required):**
|
- New endpoints
|
||||||
- Property renamed (old removed + new added)
|
- Added optional parameters
|
||||||
- Changed default values
|
- New enum values
|
||||||
- Changed validation rules (min/max length, pattern)
|
- Required → optional fields
|
||||||
- Added required request fields
|
|
||||||
- Changed parameter locations (query → body)
|
### Step 3: Impact Analysis
|
||||||
|
|
||||||
**✅ Compatible (Safe):**
|
For each breaking or warning change, analyze codebase impact:
|
||||||
- Added properties to response models
|
|
||||||
- New endpoints
|
```bash
|
||||||
- Added optional parameters
|
# Find all imports from affected API
|
||||||
- New enum values
|
grep -r "from '@generated/swagger/[api-name]" libs/ --include="*.ts"
|
||||||
- Required → optional fields
|
|
||||||
|
# Find usages of specific removed/changed items
|
||||||
### Step 3: Impact Analysis
|
grep -r "[RemovedType|removedProperty|removedMethod]" libs/*/data-access --include="*.ts"
|
||||||
|
```
|
||||||
For each breaking or warning change, analyze codebase impact:
|
|
||||||
|
**Document:**
|
||||||
```bash
|
- Affected files (with line numbers)
|
||||||
# Find all imports from affected API
|
- Services impacted
|
||||||
grep -r "from '@generated/swagger/[api-name]" libs/ --include="*.ts"
|
- Components/stores using affected services
|
||||||
|
- Estimated refactoring effort (hours)
|
||||||
# Find usages of specific removed/changed items
|
|
||||||
grep -r "[RemovedType|removedProperty|removedMethod]" libs/*/data-access --include="*.ts"
|
### Step 4: Generate Migration Strategy
|
||||||
```
|
|
||||||
|
Based on breaking change severity:
|
||||||
**Document:**
|
|
||||||
- Affected files (with line numbers)
|
**High Impact (>5 breaking changes or critical endpoints):**
|
||||||
- Services impacted
|
1. Create dedicated migration branch from develop
|
||||||
- Components/stores using affected services
|
2. Document all changes in migration guide
|
||||||
- Estimated refactoring effort (hours)
|
3. Update data-access services incrementally
|
||||||
|
4. Update affected stores and components
|
||||||
### Step 4: Generate Migration Strategy
|
5. Comprehensive test coverage
|
||||||
|
6. Coordinate deployment with backend team
|
||||||
Based on breaking change severity:
|
|
||||||
|
**Medium Impact (2-5 breaking changes):**
|
||||||
**High Impact (>5 breaking changes or critical endpoints):**
|
1. Fix TypeScript compilation errors
|
||||||
1. Create dedicated migration branch from develop
|
2. Update affected data-access services
|
||||||
2. Document all changes in migration guide
|
3. Update tests
|
||||||
3. Update data-access services incrementally
|
4. Run affected test suite
|
||||||
4. Update affected stores and components
|
5. Deploy with monitoring
|
||||||
5. Comprehensive test coverage
|
|
||||||
6. Coordinate deployment with backend team
|
**Low Impact (1 breaking change, minor impact):**
|
||||||
|
1. Apply minimal updates
|
||||||
**Medium Impact (2-5 breaking changes):**
|
2. Verify tests pass
|
||||||
1. Fix TypeScript compilation errors
|
3. Deploy normally
|
||||||
2. Update affected data-access services
|
|
||||||
3. Update tests
|
### Step 5: Execute Generation
|
||||||
4. Run affected test suite
|
|
||||||
5. Deploy with monitoring
|
If user approves proceeding after impact analysis:
|
||||||
|
|
||||||
**Low Impact (1 breaking change, minor impact):**
|
```bash
|
||||||
1. Apply minimal updates
|
# All APIs
|
||||||
2. Verify tests pass
|
npm run generate:swagger
|
||||||
3. Deploy normally
|
|
||||||
|
# Specific API (if api-name provided)
|
||||||
### Step 5: Execute Generation
|
npm run generate:swagger:[api-name]
|
||||||
|
```
|
||||||
If user approves proceeding after impact analysis:
|
|
||||||
|
### Step 6: Verify Unicode Cleanup
|
||||||
```bash
|
|
||||||
# All APIs
|
Automatic cleanup via `tools/fix-files.js` should execute during generation. Verify:
|
||||||
npm run generate:swagger
|
|
||||||
|
```bash
|
||||||
# Specific API (if api-name provided)
|
# Scan for remaining Unicode issues
|
||||||
npm run generate:swagger:[api-name]
|
grep -r "\\\\u00" generated/swagger/ || echo "✅ No Unicode issues"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 6: Verify Unicode Cleanup
|
If issues remain, run manually:
|
||||||
|
|
||||||
Automatic cleanup via `tools/fix-files.js` should execute during generation. Verify:
|
```bash
|
||||||
|
node tools/fix-files.js
|
||||||
```bash
|
```
|
||||||
# Scan for remaining Unicode issues
|
|
||||||
grep -r "\\\\u00" generated/swagger/ || echo "✅ No Unicode issues"
|
### Step 7: TypeScript Validation
|
||||||
```
|
|
||||||
|
```bash
|
||||||
If issues remain, run manually:
|
# Full TypeScript compilation check
|
||||||
|
npx tsc --noEmit
|
||||||
```bash
|
|
||||||
node tools/fix-files.js
|
# If errors, show affected files
|
||||||
```
|
npx tsc --noEmit | head -20
|
||||||
|
```
|
||||||
### Step 7: TypeScript Validation
|
|
||||||
|
Document compilation errors:
|
||||||
```bash
|
- Missing properties
|
||||||
# Full TypeScript compilation check
|
- Type mismatches
|
||||||
npx tsc --noEmit
|
- Incompatible signatures
|
||||||
|
|
||||||
# If errors, show affected files
|
### Step 8: Run Affected Tests
|
||||||
npx tsc --noEmit | head -20
|
|
||||||
```
|
```bash
|
||||||
|
# Run tests for affected projects
|
||||||
Document compilation errors:
|
npx nx affected:test --skip-nx-cache --base=HEAD~1
|
||||||
- Missing properties
|
|
||||||
- Type mismatches
|
# Lint affected projects
|
||||||
- Incompatible signatures
|
npx nx affected:lint --base=HEAD~1
|
||||||
|
```
|
||||||
### Step 8: Run Affected Tests
|
|
||||||
|
Monitor test failures and categorize:
|
||||||
```bash
|
- Mock data mismatches (update fixtures)
|
||||||
# Run tests for affected projects
|
- Type assertion failures (update test types)
|
||||||
npx nx affected:test --skip-nx-cache --base=HEAD~1
|
- Integration test failures (API contract changes)
|
||||||
|
|
||||||
# Lint affected projects
|
### Step 9: Generate Comprehensive Report
|
||||||
npx nx affected:lint --base=HEAD~1
|
|
||||||
```
|
```
|
||||||
|
API Sync Manager Report
|
||||||
Monitor test failures and categorize:
|
=======================
|
||||||
- Mock data mismatches (update fixtures)
|
|
||||||
- Type assertion failures (update test types)
|
API: [api-name | all]
|
||||||
- Integration test failures (API contract changes)
|
Sync Date: [timestamp]
|
||||||
|
Generation: ✅ Success / ❌ Failed
|
||||||
### Step 9: Generate Comprehensive Report
|
|
||||||
|
📊 Change Summary
|
||||||
```
|
-----------------
|
||||||
API Sync Manager Report
|
Breaking Changes: XX
|
||||||
=======================
|
Warnings: XX
|
||||||
|
Compatible Changes: XX
|
||||||
API: [api-name | all]
|
Files Modified: XX
|
||||||
Sync Date: [timestamp]
|
|
||||||
Generation: ✅ Success / ❌ Failed
|
🔴 Breaking Changes
|
||||||
|
-------------------
|
||||||
📊 Change Summary
|
1. [API Name] - Removed Property: OrderResponse.deliveryDate
|
||||||
-----------------
|
Impact: Medium (2 files affected)
|
||||||
Breaking Changes: XX
|
Files:
|
||||||
Warnings: XX
|
- libs/oms/data-access/src/lib/services/order.service.ts:45
|
||||||
Compatible Changes: XX
|
- libs/oms/feature/order-detail/src/lib/component.ts:78
|
||||||
Files Modified: XX
|
Fix Strategy: Remove references or use alternativeDate field
|
||||||
|
Estimated Effort: 30 minutes
|
||||||
🔴 Breaking Changes
|
|
||||||
-------------------
|
2. [API Name] - Type Changed: ProductResponse.price (string → number)
|
||||||
1. [API Name] - Removed Property: OrderResponse.deliveryDate
|
Impact: High (1 file + cascading changes)
|
||||||
Impact: Medium (2 files affected)
|
Files:
|
||||||
Files:
|
- libs/catalogue/data-access/src/lib/services/product.service.ts:32
|
||||||
- libs/oms/data-access/src/lib/services/order.service.ts:45
|
Fix Strategy: Remove string parsing, update price calculations
|
||||||
- libs/oms/feature/order-detail/src/lib/component.ts:78
|
Estimated Effort: 1 hour
|
||||||
Fix Strategy: Remove references or use alternativeDate field
|
|
||||||
Estimated Effort: 30 minutes
|
⚠️ Warnings (Review Required)
|
||||||
|
------------------------------
|
||||||
2. [API Name] - Type Changed: ProductResponse.price (string → number)
|
1. [API Name] - Possible Rename: CustomerResponse.customerName → fullName
|
||||||
Impact: High (1 file + cascading changes)
|
Action: Verify with backend team if intentional
|
||||||
Files:
|
Migration: Update references if confirmed rename
|
||||||
- libs/catalogue/data-access/src/lib/services/product.service.ts:32
|
|
||||||
Fix Strategy: Remove string parsing, update price calculations
|
2. [API Name] - New Required Field: CreateOrderRequest.taxId
|
||||||
Estimated Effort: 1 hour
|
Action: Update order creation flows to provide taxId
|
||||||
|
Impact: 3 order creation forms
|
||||||
⚠️ Warnings (Review Required)
|
|
||||||
------------------------------
|
✅ Compatible Changes
|
||||||
1. [API Name] - Possible Rename: CustomerResponse.customerName → fullName
|
---------------------
|
||||||
Action: Verify with backend team if intentional
|
1. [API Name] - Added Property: OrderResponse.estimatedDelivery
|
||||||
Migration: Update references if confirmed rename
|
2. [API Name] - New Endpoint: GET /api/v2/orders/bulk
|
||||||
|
3. [API Name] - Added Optional Parameter: includeArchived to GET /orders
|
||||||
2. [API Name] - New Required Field: CreateOrderRequest.taxId
|
|
||||||
Action: Update order creation flows to provide taxId
|
📊 Validation Results
|
||||||
Impact: 3 order creation forms
|
---------------------
|
||||||
|
TypeScript Compilation: ✅ Pass / ❌ XX errors
|
||||||
✅ Compatible Changes
|
Unicode Cleanup: ✅ Complete
|
||||||
---------------------
|
Tests: XX/XX passing (YY affected)
|
||||||
1. [API Name] - Added Property: OrderResponse.estimatedDelivery
|
Lint: ✅ Pass / ⚠️ XX warnings
|
||||||
2. [API Name] - New Endpoint: GET /api/v2/orders/bulk
|
|
||||||
3. [API Name] - Added Optional Parameter: includeArchived to GET /orders
|
❌ Test Failures (if any)
|
||||||
|
-------------------------
|
||||||
📊 Validation Results
|
1. order.service.spec.ts - Mock response missing deliveryDate
|
||||||
---------------------
|
Fix: Update mock fixture
|
||||||
TypeScript Compilation: ✅ Pass / ❌ XX errors
|
2. product.component.spec.ts - Price type assertion failed
|
||||||
Unicode Cleanup: ✅ Complete
|
Fix: Change expect(price).toBe("10.99") → expect(price).toBe(10.99)
|
||||||
Tests: XX/XX passing (YY affected)
|
|
||||||
Lint: ✅ Pass / ⚠️ XX warnings
|
💡 Migration Strategy
|
||||||
|
---------------------
|
||||||
❌ Test Failures (if any)
|
Approach: [High/Medium/Low Impact]
|
||||||
-------------------------
|
Estimated Total Effort: [hours]
|
||||||
1. order.service.spec.ts - Mock response missing deliveryDate
|
|
||||||
Fix: Update mock fixture
|
Steps:
|
||||||
2. product.component.spec.ts - Price type assertion failed
|
1. [Fix compilation errors in data-access services]
|
||||||
Fix: Change expect(price).toBe("10.99") → expect(price).toBe(10.99)
|
2. [Update test mocks and fixtures]
|
||||||
|
3. [Update components using affected properties]
|
||||||
💡 Migration Strategy
|
4. [Run full test suite]
|
||||||
---------------------
|
5. [Deploy with backend coordination]
|
||||||
Approach: [High/Medium/Low Impact]
|
|
||||||
Estimated Total Effort: [hours]
|
🎯 Recommendation
|
||||||
|
-----------------
|
||||||
Steps:
|
[One of the following:]
|
||||||
1. [Fix compilation errors in data-access services]
|
✅ Safe to proceed - Only compatible changes detected
|
||||||
2. [Update test mocks and fixtures]
|
⚠️ Proceed with caution - Fix breaking changes before deployment
|
||||||
3. [Update components using affected properties]
|
🔴 Coordinate with backend - High impact changes require migration plan
|
||||||
4. [Run full test suite]
|
🛑 Block regeneration - Critical breaking changes, backend rollback needed
|
||||||
5. [Deploy with backend coordination]
|
|
||||||
|
📋 Next Steps
|
||||||
🎯 Recommendation
|
-------------
|
||||||
-----------------
|
[Specific actions user should take]
|
||||||
[One of the following:]
|
- [ ] Fix compilation errors in [files]
|
||||||
✅ Safe to proceed - Only compatible changes detected
|
- [ ] Update test mocks in [files]
|
||||||
⚠️ Proceed with caution - Fix breaking changes before deployment
|
- [ ] Coordinate deployment timing with backend team
|
||||||
🔴 Coordinate with backend - High impact changes require migration plan
|
- [ ] Monitor [specific endpoints] after deployment
|
||||||
🛑 Block regeneration - Critical breaking changes, backend rollback needed
|
```
|
||||||
|
|
||||||
📋 Next Steps
|
### Step 10: Cleanup
|
||||||
-------------
|
|
||||||
[Specific actions user should take]
|
```bash
|
||||||
- [ ] Fix compilation errors in [files]
|
# Remove temporary backup
|
||||||
- [ ] Update test mocks in [files]
|
rm -rf /tmp/[api-name].backup
|
||||||
- [ ] Coordinate deployment timing with backend team
|
|
||||||
- [ ] Monitor [specific endpoints] after deployment
|
# Or restore if generation needs to be reverted
|
||||||
```
|
# cp -r /tmp/[api-name].backup generated/swagger/[api-name]
|
||||||
|
```
|
||||||
### Step 10: Cleanup
|
|
||||||
|
## Error Handling
|
||||||
```bash
|
|
||||||
# Remove temporary backup
|
**Generation Fails:**
|
||||||
rm -rf /tmp/[api-name].backup
|
- Check OpenAPI spec URLs in package.json scripts
|
||||||
|
- Verify backend API is accessible
|
||||||
# Or restore if generation needs to be reverted
|
- Check for malformed OpenAPI specification
|
||||||
# cp -r /tmp/[api-name].backup generated/swagger/[api-name]
|
|
||||||
```
|
**Unicode Cleanup Fails:**
|
||||||
|
- Run `node tools/fix-files.js` manually
|
||||||
## Error Handling
|
- Check for new Unicode patterns not covered by script
|
||||||
|
|
||||||
**Generation Fails:**
|
**TypeScript Compilation Errors:**
|
||||||
- Check OpenAPI spec URLs in package.json scripts
|
- Review breaking changes section in report
|
||||||
- Verify backend API is accessible
|
- Update affected data-access services first
|
||||||
- Check for malformed OpenAPI specification
|
- Fix cascading type errors in consumers
|
||||||
|
|
||||||
**Unicode Cleanup Fails:**
|
**Test Failures:**
|
||||||
- Run `node tools/fix-files.js` manually
|
- Update mock data to match new API contracts
|
||||||
- Check for new Unicode patterns not covered by script
|
- Fix type assertions in tests
|
||||||
|
- Update integration test expectations
|
||||||
**TypeScript Compilation Errors:**
|
|
||||||
- Review breaking changes section in report
|
**Diff Detection Issues:**
|
||||||
- Update affected data-access services first
|
- Ensure clean git state before running
|
||||||
- Fix cascading type errors in consumers
|
- Check file permissions on generated/ directory
|
||||||
|
- Verify backup location is writable
|
||||||
**Test Failures:**
|
|
||||||
- Update mock data to match new API contracts
|
## Advanced Usage
|
||||||
- Fix type assertions in tests
|
|
||||||
- Update integration test expectations
|
**Compare Specific Endpoints Only:**
|
||||||
|
|
||||||
**Diff Detection Issues:**
|
```bash
|
||||||
- Ensure clean git state before running
|
# Extract and compare specific interface
|
||||||
- Check file permissions on generated/ directory
|
grep -A 20 "interface OrderResponse" /tmp/[api-name].backup/models.ts
|
||||||
- Verify backup location is writable
|
grep -A 20 "interface OrderResponse" generated/swagger/[api-name]/models.ts
|
||||||
|
```
|
||||||
## Advanced Usage
|
|
||||||
|
**Bulk API Sync with Change Detection:**
|
||||||
**Compare Specific Endpoints Only:**
|
|
||||||
|
Run pre-generation detection for all APIs before regenerating:
|
||||||
```bash
|
|
||||||
# Extract and compare specific interface
|
```bash
|
||||||
grep -A 20 "interface OrderResponse" /tmp/[api-name].backup/models.ts
|
for api in availability-api cat-search-api checkout-api crm-api eis-api inventory-api isa-api oms-api print-api wws-api; do
|
||||||
grep -A 20 "interface OrderResponse" generated/swagger/[api-name]/models.ts
|
echo "Checking $api..."
|
||||||
```
|
# Run detection workflow for each
|
||||||
|
done
|
||||||
**Bulk API Sync with Change Detection:**
|
```
|
||||||
|
|
||||||
Run pre-generation detection for all APIs before regenerating:
|
Generate consolidated report across all APIs before committing.
|
||||||
|
|
||||||
```bash
|
**Rollback Procedure:**
|
||||||
for api in availability-api cat-search-api checkout-api crm-api eis-api inventory-api isa-api oms-api print-api wws-api; do
|
|
||||||
echo "Checking $api..."
|
```bash
|
||||||
# Run detection workflow for each
|
# If sync causes critical issues
|
||||||
done
|
git checkout generated/swagger/[api-name]
|
||||||
```
|
git clean -fd generated/swagger/[api-name]
|
||||||
|
|
||||||
Generate consolidated report across all APIs before committing.
|
# Or restore from backup
|
||||||
|
cp -r generated/swagger.backup.[timestamp]/* generated/swagger/
|
||||||
**Rollback Procedure:**
|
```
|
||||||
|
|
||||||
```bash
|
## Integration with Git Workflow
|
||||||
# If sync causes critical issues
|
|
||||||
git checkout generated/swagger/[api-name]
|
**Recommended Commit Message:**
|
||||||
git clean -fd generated/swagger/[api-name]
|
|
||||||
|
```
|
||||||
# Or restore from backup
|
feat(api): sync [api-name] API client [TASK-####]
|
||||||
cp -r generated/swagger.backup.[timestamp]/* generated/swagger/
|
|
||||||
```
|
Breaking changes:
|
||||||
|
- Removed OrderResponse.deliveryDate (use estimatedDelivery)
|
||||||
## Integration with Git Workflow
|
- Changed ProductResponse.price type (string → number)
|
||||||
|
|
||||||
**Recommended Commit Message:**
|
Compatible changes:
|
||||||
|
- Added OrderResponse.estimatedDelivery
|
||||||
```
|
- New endpoint GET /api/v2/orders/bulk
|
||||||
feat(api): sync [api-name] API client [TASK-####]
|
```
|
||||||
|
|
||||||
Breaking changes:
|
**Branch Strategy:**
|
||||||
- Removed OrderResponse.deliveryDate (use estimatedDelivery)
|
|
||||||
- Changed ProductResponse.price type (string → number)
|
- Low impact: Commit to feature branch directly
|
||||||
|
- Medium impact: Create `sync/[api-name]-[date]` branch
|
||||||
Compatible changes:
|
- High impact: Create `migration/[api-name]-[version]` branch with migration guide
|
||||||
- Added OrderResponse.estimatedDelivery
|
|
||||||
- New endpoint GET /api/v2/orders/bulk
|
## References
|
||||||
```
|
|
||||||
|
- CLAUDE.md API Integration section
|
||||||
**Branch Strategy:**
|
- package.json swagger generation scripts
|
||||||
|
- tools/fix-files.js for Unicode cleanup logic
|
||||||
- Low impact: Commit to feature branch directly
|
- Semantic Versioning: https://semver.org
|
||||||
- Medium impact: Create `sync/[api-name]-[date]` branch
|
|
||||||
- High impact: Create `migration/[api-name]-[version]` branch with migration guide
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- CLAUDE.md API Integration section
|
|
||||||
- package.json swagger generation scripts
|
|
||||||
- tools/fix-files.js for Unicode cleanup logic
|
|
||||||
- Semantic Versioning: https://semver.org
|
|
||||||
|
|||||||
@@ -1,171 +1,177 @@
|
|||||||
---
|
---
|
||||||
name: arch-docs
|
name: arch-docs
|
||||||
description: Generate architecture documentation (C4, Arc42, ADRs, PlantUML). Auto-invoke when user mentions "architecture docs", "C4 model", "ADR", "document architecture", "system design", or "create architecture diagram".
|
description: Generate architecture documentation (C4, Arc42, ADRs, PlantUML). Auto-invoke when user mentions "architecture docs", "C4 model", "ADR", "document architecture", "system design", or "create architecture diagram".
|
||||||
---
|
---
|
||||||
|
|
||||||
# Architecture Documentation Skill
|
# Architecture Documentation
|
||||||
|
|
||||||
Generate comprehensive architecture documentation using modern frameworks and best practices.
|
Generate comprehensive architecture documentation using modern frameworks and best practices.
|
||||||
|
|
||||||
## When to Use
|
## Available Frameworks
|
||||||
|
|
||||||
- Creating or updating architecture documentation
|
### C4 Model
|
||||||
- Generating C4 model diagrams (Context, Container, Component, Code)
|
Visualize software architecture at different abstraction levels.
|
||||||
- Writing Architecture Decision Records (ADRs)
|
|
||||||
- Documenting system design and component relationships
|
**Levels:**
|
||||||
- Creating PlantUML or Mermaid diagrams
|
1. **Context** - System landscape and external actors
|
||||||
|
2. **Container** - High-level technology choices (apps, databases, etc.)
|
||||||
## Available Frameworks
|
3. **Component** - Internal structure of containers
|
||||||
|
4. **Code** - Class/module level detail (optional)
|
||||||
### C4 Model
|
|
||||||
Best for: Visualizing software architecture at different abstraction levels
|
**When to use:** Visualizing system architecture, creating diagrams for stakeholders at different technical levels.
|
||||||
|
|
||||||
Levels:
|
See `references/c4-model.md` for detailed patterns and examples.
|
||||||
1. **Context** - System landscape and external actors
|
|
||||||
2. **Container** - High-level technology choices (apps, databases, etc.)
|
### Arc42 Template
|
||||||
3. **Component** - Internal structure of containers
|
Comprehensive architecture documentation covering all aspects.
|
||||||
4. **Code** - Class/module level detail (optional)
|
|
||||||
|
**Sections:**
|
||||||
See: `@references/c4-model.md` for patterns and examples
|
1. Introduction and Goals
|
||||||
|
2. Constraints
|
||||||
### Arc42 Template
|
3. Context and Scope
|
||||||
Best for: Comprehensive architecture documentation
|
4. Solution Strategy
|
||||||
|
5. Building Block View
|
||||||
Sections:
|
6. Runtime View
|
||||||
1. Introduction and Goals
|
7. Deployment View
|
||||||
2. Constraints
|
8. Cross-cutting Concepts
|
||||||
3. Context and Scope
|
9. Architecture Decisions
|
||||||
4. Solution Strategy
|
10. Quality Requirements
|
||||||
5. Building Block View
|
11. Risks and Technical Debt
|
||||||
6. Runtime View
|
12. Glossary
|
||||||
7. Deployment View
|
|
||||||
8. Cross-cutting Concepts
|
**When to use:** Creating complete architecture documentation, documenting system-wide concerns, establishing quality goals.
|
||||||
9. Architecture Decisions
|
|
||||||
10. Quality Requirements
|
See `references/arc42.md` for complete template structure.
|
||||||
11. Risks and Technical Debt
|
|
||||||
12. Glossary
|
### Architecture Decision Records (ADRs)
|
||||||
|
Document individual architectural decisions with context and consequences.
|
||||||
See: `@references/arc42.md` for template structure
|
|
||||||
|
**When to use:** Recording significant decisions, documenting trade-offs, tracking architectural evolution.
|
||||||
### Architecture Decision Records (ADRs)
|
|
||||||
Best for: Documenting individual architectural decisions
|
See `references/adr-template.md` for format and examples.
|
||||||
|
|
||||||
See: `@references/adr-template.md` for format and examples
|
## Workflow
|
||||||
|
|
||||||
## Workflow
|
### 1. Discovery Phase
|
||||||
|
Understand the system structure:
|
||||||
### 1. Discovery Phase
|
|
||||||
```bash
|
```bash
|
||||||
# Find existing architecture files
|
# Find existing architecture files
|
||||||
find . -name "*architecture*" -o -name "*.puml" -o -name "*.mmd"
|
find . -name "*architecture*" -o -name "*.puml" -o -name "*.mmd"
|
||||||
|
|
||||||
# Identify service boundaries
|
# Identify service boundaries
|
||||||
cat nx.json docker-compose.yml
|
cat nx.json docker-compose.yml
|
||||||
|
|
||||||
# Check for existing ADRs
|
# Check for existing ADRs
|
||||||
ls -la docs/adr/ docs/decisions/
|
ls -la docs/adr/ docs/decisions/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Analysis Phase
|
### 2. Analysis Phase
|
||||||
- Analyze codebase structure (`libs/`, `apps/`)
|
- Analyze codebase structure (`libs/`, `apps/`)
|
||||||
- Identify dependencies from `tsconfig.base.json` paths
|
- Identify dependencies from `tsconfig.base.json` paths
|
||||||
- Review service boundaries from `project.json` tags
|
- Review service boundaries from `project.json` tags
|
||||||
- Map data flow from API definitions
|
- Map data flow from API definitions
|
||||||
|
|
||||||
### 3. Documentation Phase
|
### 3. Documentation Phase
|
||||||
Based on the request, create appropriate documentation:
|
Choose appropriate output format based on request:
|
||||||
|
|
||||||
**For C4 diagrams:**
|
**For C4 diagrams:**
|
||||||
```
|
```
|
||||||
docs/architecture/
|
docs/architecture/
|
||||||
├── c4-context.puml
|
├── c4-context.puml
|
||||||
├── c4-container.puml
|
├── c4-container.puml
|
||||||
└── c4-component-[name].puml
|
└── c4-component-[name].puml
|
||||||
```
|
```
|
||||||
|
|
||||||
**For ADRs:**
|
**For ADRs:**
|
||||||
```
|
```
|
||||||
docs/adr/
|
docs/adr/
|
||||||
├── 0001-record-architecture-decisions.md
|
├── 0001-record-architecture-decisions.md
|
||||||
├── 0002-[decision-title].md
|
├── 0002-[decision-title].md
|
||||||
└── template.md
|
└── template.md
|
||||||
```
|
```
|
||||||
|
|
||||||
**For Arc42:**
|
**For Arc42:**
|
||||||
```
|
```
|
||||||
docs/architecture/
|
docs/architecture/
|
||||||
└── arc42/
|
└── arc42/
|
||||||
├── 01-introduction.md
|
├── 01-introduction.md
|
||||||
├── 02-constraints.md
|
├── 02-constraints.md
|
||||||
└── ...
|
└── ...
|
||||||
```
|
```
|
||||||
|
|
||||||
## ISA-Frontend Specific Context
|
## ISA-Frontend Context
|
||||||
|
|
||||||
### Monorepo Structure
|
### Monorepo Structure
|
||||||
- **apps/**: Angular applications
|
- **apps/**: Angular applications
|
||||||
- **libs/**: Shared libraries organized by domain
|
- **libs/**: Shared libraries organized by domain
|
||||||
- `libs/[domain]/feature/` - Feature modules
|
- `libs/[domain]/feature/` - Feature modules
|
||||||
- `libs/[domain]/data-access/` - State management
|
- `libs/[domain]/data-access/` - State management
|
||||||
- `libs/[domain]/ui/` - Presentational components
|
- `libs/[domain]/ui/` - Presentational components
|
||||||
- `libs/[domain]/util/` - Utilities
|
- `libs/[domain]/util/` - Utilities
|
||||||
|
|
||||||
### Key Architectural Patterns
|
### Key Architectural Patterns
|
||||||
- **Nx Monorepo** with strict module boundaries
|
- **Nx Monorepo** with strict module boundaries
|
||||||
- **NgRx Signal Store** for state management
|
- **NgRx Signal Store** for state management
|
||||||
- **Standalone Components** (Angular 20+)
|
- **Standalone Components** (Angular 20+)
|
||||||
- **Domain-Driven Design** library organization
|
- **Domain-Driven Design** library organization
|
||||||
|
|
||||||
### Documentation Locations
|
### Documentation Locations
|
||||||
- ADRs: `docs/adr/`
|
- ADRs: `docs/adr/`
|
||||||
- Architecture diagrams: `docs/architecture/`
|
- Architecture diagrams: `docs/architecture/`
|
||||||
- API documentation: Generated from Swagger/OpenAPI
|
- API documentation: Generated from Swagger/OpenAPI
|
||||||
|
|
||||||
## Output Standards
|
## Output Standards
|
||||||
|
|
||||||
### PlantUML Format
|
### PlantUML Format
|
||||||
```plantuml
|
```plantuml
|
||||||
@startuml C4_Context
|
@startuml C4_Context
|
||||||
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
|
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
|
||||||
|
|
||||||
Person(user, "User", "System user")
|
Person(user, "User", "System user")
|
||||||
System(system, "ISA System", "Main application")
|
System(system, "ISA System", "Main application")
|
||||||
System_Ext(external, "External API", "Third-party service")
|
System_Ext(external, "External API", "Third-party service")
|
||||||
|
|
||||||
Rel(user, system, "Uses")
|
Rel(user, system, "Uses")
|
||||||
Rel(system, external, "Calls")
|
Rel(system, external, "Calls")
|
||||||
@enduml
|
@enduml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mermaid Format
|
### Mermaid Format
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
A[User] --> B[ISA App]
|
A[User] --> B[ISA App]
|
||||||
B --> C[API Gateway]
|
B --> C[API Gateway]
|
||||||
C --> D[Backend Services]
|
C --> D[Backend Services]
|
||||||
```
|
```
|
||||||
|
|
||||||
### ADR Format
|
### ADR Format
|
||||||
```markdown
|
```markdown
|
||||||
# ADR-XXXX: [Title]
|
# ADR-XXXX: [Title]
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
[Proposed | Accepted | Deprecated | Superseded]
|
[Proposed | Accepted | Deprecated | Superseded]
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
[What is the issue?]
|
[What is the issue?]
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
[What was decided?]
|
[What was decided?]
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
[What are the results?]
|
[What are the results?]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
1. **Start with Context** - Always begin with C4 Level 1 (System Context)
|
1. **Start with Context** - Always begin with C4 Level 1 (System Context)
|
||||||
2. **Use Consistent Notation** - Stick to one diagramming tool/format
|
2. **Use Consistent Notation** - Stick to one diagramming tool/format
|
||||||
3. **Keep ADRs Atomic** - One decision per ADR
|
3. **Keep ADRs Atomic** - One decision per ADR
|
||||||
4. **Version Control** - Commit documentation with code changes
|
4. **Version Control** - Commit documentation with code changes
|
||||||
5. **Review Regularly** - Architecture docs decay; schedule reviews
|
5. **Review Regularly** - Architecture docs decay; schedule reviews
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `references/c4-model.md` - C4 model patterns, templates, and ISA-Frontend domain structure
|
||||||
|
- `references/arc42.md` - Complete Arc42 template with all 12 sections
|
||||||
|
- `references/adr-template.md` - ADR format with examples and naming conventions
|
||||||
|
|||||||
@@ -9,16 +9,6 @@ description: This skill should be used when validating architecture compliance,
|
|||||||
|
|
||||||
Validate and enforce architectural boundaries in the ISA-Frontend monorepo. Detects import boundary violations, circular dependencies, layer violations, and cross-domain dependencies. Provides automated fix strategies using graph analysis, dependency injection, interface extraction, and shared code refactoring.
|
Validate and enforce architectural boundaries in the ISA-Frontend monorepo. Detects import boundary violations, circular dependencies, layer violations, and cross-domain dependencies. Provides automated fix strategies using graph analysis, dependency injection, interface extraction, and shared code refactoring.
|
||||||
|
|
||||||
## When to Use This Skill
|
|
||||||
|
|
||||||
Invoke when user wants to:
|
|
||||||
- Validate import boundaries or architectural rules
|
|
||||||
- Detect circular dependencies or dependency cycles
|
|
||||||
- Find layer violations (Feature→Feature, Data Access→Feature)
|
|
||||||
- Check cross-domain dependencies
|
|
||||||
- Resolve build failures with circular import warnings
|
|
||||||
- Mentioned "check architecture", "validate boundaries", "circular dependencies", or "dependency cycles"
|
|
||||||
|
|
||||||
## Architectural Rules
|
## Architectural Rules
|
||||||
|
|
||||||
**Allowed Dependencies:**
|
**Allowed Dependencies:**
|
||||||
|
|||||||
@@ -9,17 +9,6 @@ description: This skill should be used when writing or reviewing CSS animations
|
|||||||
|
|
||||||
Implement native CSS @keyframes animations for Angular applications, replacing @angular/animations with GPU-accelerated, zero-bundle-size alternatives. This skill provides comprehensive guidance on creating performant entrance/exit animations, staggered effects, and proper timing configurations.
|
Implement native CSS @keyframes animations for Angular applications, replacing @angular/animations with GPU-accelerated, zero-bundle-size alternatives. This skill provides comprehensive guidance on creating performant entrance/exit animations, staggered effects, and proper timing configurations.
|
||||||
|
|
||||||
## When to Use This Skill
|
|
||||||
|
|
||||||
Apply this skill when:
|
|
||||||
- **Writing Angular components** with entrance/exit animations
|
|
||||||
- **Converting @angular/animations** to native CSS @keyframes
|
|
||||||
- **Implementing animate.enter/animate.leave** in Angular 20+ templates
|
|
||||||
- **Creating staggered animations** for lists or collections
|
|
||||||
- **Debugging animation issues** (snap-back, wrong starting positions, choppy playback)
|
|
||||||
- **Optimizing animation performance** for GPU acceleration
|
|
||||||
- **Reviewing animation code** for accessibility and best practices
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Basic Animation Setup
|
### Basic Animation Setup
|
||||||
@@ -311,41 +300,6 @@ element.addEventListener('animationend', (e) => {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
### references/keyframes-guide.md
|
|
||||||
|
|
||||||
Comprehensive deep-dive covering:
|
|
||||||
- Complete @keyframes syntax reference
|
|
||||||
- Detailed timing functions and cubic-bezier curves
|
|
||||||
- Advanced techniques (direction, play-state, @starting-style)
|
|
||||||
- Performance optimization strategies
|
|
||||||
- Extensive common patterns library
|
|
||||||
- Debugging techniques and troubleshooting
|
|
||||||
|
|
||||||
**When to reference:** Complex animation requirements, custom easing curves, advanced techniques, performance optimization, or learning comprehensive details.
|
|
||||||
|
|
||||||
### assets/animations.css
|
|
||||||
|
|
||||||
Ready-to-use CSS file with common animation patterns:
|
|
||||||
- Fade animations (in/out)
|
|
||||||
- Slide animations (up/down/left/right)
|
|
||||||
- Scale animations (in/out)
|
|
||||||
- Utility animations (spin, shimmer, shake, breathe, attention-pulse)
|
|
||||||
- Toast/notification animations
|
|
||||||
- Accessibility (@media prefers-reduced-motion)
|
|
||||||
|
|
||||||
**Usage:** Copy this file to project and import in component styles or global styles:
|
|
||||||
|
|
||||||
```css
|
|
||||||
@import 'path/to/animations.css';
|
|
||||||
```
|
|
||||||
|
|
||||||
Then use classes directly:
|
|
||||||
```html
|
|
||||||
<div animate.enter="fade-in" animate.leave="slide-out-down">
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration from @angular/animations
|
## Migration from @angular/animations
|
||||||
|
|
||||||
### Before (Angular Animations)
|
### Before (Angular Animations)
|
||||||
@@ -390,3 +344,38 @@ import { trigger, state, style, transition, animate } from '@angular/animations'
|
|||||||
- GPU hardware acceleration
|
- GPU hardware acceleration
|
||||||
- Standard CSS (transferable skills)
|
- Standard CSS (transferable skills)
|
||||||
- Better performance
|
- Better performance
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### references/keyframes-guide.md
|
||||||
|
|
||||||
|
Comprehensive deep-dive covering:
|
||||||
|
- Complete @keyframes syntax reference
|
||||||
|
- Detailed timing functions and cubic-bezier curves
|
||||||
|
- Advanced techniques (direction, play-state, @starting-style)
|
||||||
|
- Performance optimization strategies
|
||||||
|
- Extensive common patterns library
|
||||||
|
- Debugging techniques and troubleshooting
|
||||||
|
|
||||||
|
**When to reference:** Complex animation requirements, custom easing curves, advanced techniques, performance optimization, or learning comprehensive details.
|
||||||
|
|
||||||
|
### assets/animations.css
|
||||||
|
|
||||||
|
Ready-to-use CSS file with common animation patterns:
|
||||||
|
- Fade animations (in/out)
|
||||||
|
- Slide animations (up/down/left/right)
|
||||||
|
- Scale animations (in/out)
|
||||||
|
- Utility animations (spin, shimmer, shake, breathe, attention-pulse)
|
||||||
|
- Toast/notification animations
|
||||||
|
- Accessibility (@media prefers-reduced-motion)
|
||||||
|
|
||||||
|
**Usage:** Copy this file to project and import in component styles or global styles:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@import 'path/to/animations.css';
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use classes directly:
|
||||||
|
```html
|
||||||
|
<div animate.enter="fade-in" animate.leave="slide-out-down">
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,352 +1,352 @@
|
|||||||
---
|
---
|
||||||
name: git-workflow
|
name: git-workflow
|
||||||
description: This skill should be used when creating branches, writing commits, or creating pull requests. Enforces ISA-Frontend Git conventions including feature/task-id-name branch format, conventional commits without co-author tags, and PRs targeting develop branch.
|
description: This skill should be used when creating branches, writing commits, or creating pull requests. Enforces ISA-Frontend Git conventions including feature/task-id-name branch format, conventional commits without co-author tags, and PRs targeting develop branch.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Git Workflow Skill
|
# Git Workflow Skill
|
||||||
|
|
||||||
Enforces Git workflow conventions specific to the ISA-Frontend project.
|
Enforces Git workflow conventions specific to the ISA-Frontend project.
|
||||||
|
|
||||||
## When to Use
|
## When to Use
|
||||||
|
|
||||||
- Creating new branches for features or bugfixes
|
- Creating new branches for features or bugfixes
|
||||||
- Writing commit messages
|
- Writing commit messages
|
||||||
- Creating pull requests
|
- Creating pull requests
|
||||||
- Any Git operations requiring adherence to project conventions
|
- Any Git operations requiring adherence to project conventions
|
||||||
|
|
||||||
## Core Principles
|
## Core Principles
|
||||||
|
|
||||||
### 1. Default Branch is `develop` (NOT `main`)
|
### 1. Default Branch is `develop` (NOT `main`)
|
||||||
|
|
||||||
- **All PRs target**: `develop` branch
|
- **All PRs target**: `develop` branch
|
||||||
- **Feature branches from**: `develop`
|
- **Feature branches from**: `develop`
|
||||||
- **Never push directly to**: `develop` or `main`
|
- **Never push directly to**: `develop` or `main`
|
||||||
|
|
||||||
### 2. Branch Naming Convention
|
### 2. Branch Naming Convention
|
||||||
|
|
||||||
**Format**: `<type>/{task-id}-{short-description}`
|
**Format**: `<type>/{task-id}-{short-description}`
|
||||||
|
|
||||||
**Types**:
|
**Types**:
|
||||||
- `feature/` - New features or enhancements
|
- `feature/` - New features or enhancements
|
||||||
- `bugfix/` - Bug fixes
|
- `bugfix/` - Bug fixes
|
||||||
- `hotfix/` - Emergency production fixes
|
- `hotfix/` - Emergency production fixes
|
||||||
|
|
||||||
**Rules**:
|
**Rules**:
|
||||||
- Use English kebab-case for descriptions
|
- Use English kebab-case for descriptions
|
||||||
- Start with task/issue ID (e.g., `5391`)
|
- Start with task/issue ID (e.g., `5391`)
|
||||||
- Keep description concise - shorten if too long
|
- Keep description concise - shorten if too long
|
||||||
- Use hyphens to separate words
|
- Use hyphens to separate words
|
||||||
|
|
||||||
**Examples**:
|
**Examples**:
|
||||||
```bash
|
```bash
|
||||||
# Good
|
# Good
|
||||||
feature/5391-praemie-checkout-action-card-delivery-order
|
feature/5391-praemie-checkout-action-card-delivery-order
|
||||||
bugfix/6123-fix-login-redirect-loop
|
bugfix/6123-fix-login-redirect-loop
|
||||||
hotfix/7890-critical-payment-error
|
hotfix/7890-critical-payment-error
|
||||||
|
|
||||||
# Bad
|
# Bad
|
||||||
feature/praemie-checkout # Missing task ID
|
feature/praemie-checkout # Missing task ID
|
||||||
feature/5391_praemie # Using underscores
|
feature/5391_praemie # Using underscores
|
||||||
feature-5391-very-long-description-that-goes-on-forever # Too long
|
feature-5391-very-long-description-that-goes-on-forever # Too long
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Conventional Commits (WITHOUT Co-Author Tags)
|
### 3. Conventional Commits (WITHOUT Co-Author Tags)
|
||||||
|
|
||||||
**Format**: `<type>(<scope>): <description>`
|
**Format**: `<type>(<scope>): <description>`
|
||||||
|
|
||||||
**Types**:
|
**Types**:
|
||||||
- `feat`: New feature
|
- `feat`: New feature
|
||||||
- `fix`: Bug fix
|
- `fix`: Bug fix
|
||||||
- `docs`: Documentation only
|
- `docs`: Documentation only
|
||||||
- `style`: Code style (formatting, missing semicolons)
|
- `style`: Code style (formatting, missing semicolons)
|
||||||
- `refactor`: Code restructuring without feature changes
|
- `refactor`: Code restructuring without feature changes
|
||||||
- `perf`: Performance improvements
|
- `perf`: Performance improvements
|
||||||
- `test`: Adding or updating tests
|
- `test`: Adding or updating tests
|
||||||
- `build`: Build system or dependencies
|
- `build`: Build system or dependencies
|
||||||
- `ci`: CI configuration
|
- `ci`: CI configuration
|
||||||
- `chore`: Maintenance tasks
|
- `chore`: Maintenance tasks
|
||||||
|
|
||||||
**Rules**:
|
**Rules**:
|
||||||
- ❌ **NO** "Generated with Claude Code" tags
|
- ❌ **NO** "Generated with Claude Code" tags
|
||||||
- ❌ **NO** "Co-Authored-By: Claude" tags
|
- ❌ **NO** "Co-Authored-By: Claude" tags
|
||||||
- ✅ Keep first line under 72 characters
|
- ✅ Keep first line under 72 characters
|
||||||
- ✅ Use imperative mood ("add" not "added")
|
- ✅ Use imperative mood ("add" not "added")
|
||||||
- ✅ Body optional but recommended for complex changes
|
- ✅ Body optional but recommended for complex changes
|
||||||
|
|
||||||
**Examples**:
|
**Examples**:
|
||||||
```bash
|
```bash
|
||||||
# Good
|
# Good
|
||||||
feat(checkout): add bonus card selection for delivery orders
|
feat(checkout): add bonus card selection for delivery orders
|
||||||
|
|
||||||
fix(crm): resolve customer search filter reset issue
|
fix(crm): resolve customer search filter reset issue
|
||||||
|
|
||||||
refactor(oms): extract return validation logic into service
|
refactor(oms): extract return validation logic into service
|
||||||
|
|
||||||
# Bad
|
# Bad
|
||||||
feat(checkout): add bonus card selection
|
feat(checkout): add bonus card selection
|
||||||
|
|
||||||
Generated with Claude Code
|
Generated with Claude Code
|
||||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||||
|
|
||||||
# Also bad
|
# Also bad
|
||||||
Added new feature # Wrong tense
|
Added new feature # Wrong tense
|
||||||
Fix bug # Missing scope
|
Fix bug # Missing scope
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Pull Request Creation
|
### 4. Pull Request Creation
|
||||||
|
|
||||||
**Target Branch**: Always `develop`
|
**Target Branch**: Always `develop`
|
||||||
|
|
||||||
**PR Title Format**: Same as conventional commit
|
**PR Title Format**: Same as conventional commit
|
||||||
```
|
```
|
||||||
feat(domain): concise description of changes
|
feat(domain): concise description of changes
|
||||||
```
|
```
|
||||||
|
|
||||||
**PR Body Structure**:
|
**PR Body Structure**:
|
||||||
```markdown
|
```markdown
|
||||||
## Summary
|
## Summary
|
||||||
- Brief bullet points of changes
|
- Brief bullet points of changes
|
||||||
|
|
||||||
## Related Tasks
|
## Related Tasks
|
||||||
- Closes #{task-id}
|
- Closes #{task-id}
|
||||||
- Refs #{related-task}
|
- Refs #{related-task}
|
||||||
|
|
||||||
## Test Plan
|
## Test Plan
|
||||||
- [ ] Unit tests added/updated
|
- [ ] Unit tests added/updated
|
||||||
- [ ] E2E attributes added
|
- [ ] E2E attributes added
|
||||||
- [ ] Manual testing completed
|
- [ ] Manual testing completed
|
||||||
|
|
||||||
## Breaking Changes
|
## Breaking Changes
|
||||||
None / List breaking changes
|
None / List breaking changes
|
||||||
|
|
||||||
## Screenshots (if UI changes)
|
## Screenshots (if UI changes)
|
||||||
[Add screenshots]
|
[Add screenshots]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Workflows
|
## Common Workflows
|
||||||
|
|
||||||
### Creating a Feature Branch
|
### Creating a Feature Branch
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Update develop
|
# 1. Update develop
|
||||||
git checkout develop
|
git checkout develop
|
||||||
git pull origin develop
|
git pull origin develop
|
||||||
|
|
||||||
# 2. Create feature branch
|
# 2. Create feature branch
|
||||||
git checkout -b feature/5391-praemie-checkout-action-card
|
git checkout -b feature/5391-praemie-checkout-action-card
|
||||||
|
|
||||||
# 3. Work and commit
|
# 3. Work and commit
|
||||||
git add .
|
git add .
|
||||||
git commit -m "feat(checkout): add primary bonus card selection logic"
|
git commit -m "feat(checkout): add primary bonus card selection logic"
|
||||||
|
|
||||||
# 4. Push to remote
|
# 4. Push to remote
|
||||||
git push -u origin feature/5391-praemie-checkout-action-card
|
git push -u origin feature/5391-praemie-checkout-action-card
|
||||||
|
|
||||||
# 5. Create PR targeting develop (use gh CLI or web UI)
|
# 5. Create PR targeting develop (use gh CLI or web UI)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Creating a Bugfix Branch
|
### Creating a Bugfix Branch
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From develop
|
# From develop
|
||||||
git checkout develop
|
git checkout develop
|
||||||
git pull origin develop
|
git pull origin develop
|
||||||
git checkout -b bugfix/6123-login-redirect-loop
|
git checkout -b bugfix/6123-login-redirect-loop
|
||||||
|
|
||||||
# Commit
|
# Commit
|
||||||
git commit -m "fix(auth): resolve infinite redirect on logout"
|
git commit -m "fix(auth): resolve infinite redirect on logout"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Creating a Hotfix Branch
|
### Creating a Hotfix Branch
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From main (production)
|
# From main (production)
|
||||||
git checkout main
|
git checkout main
|
||||||
git pull origin main
|
git pull origin main
|
||||||
git checkout -b hotfix/7890-payment-processing-error
|
git checkout -b hotfix/7890-payment-processing-error
|
||||||
|
|
||||||
# Commit
|
# Commit
|
||||||
git commit -m "fix(checkout): critical payment API timeout handling"
|
git commit -m "fix(checkout): critical payment API timeout handling"
|
||||||
|
|
||||||
# Merge to both main and develop
|
# Merge to both main and develop
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commit Message Guidelines
|
## Commit Message Guidelines
|
||||||
|
|
||||||
### Good Commit Messages
|
### Good Commit Messages
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
feat(crm): add customer loyalty tier calculation
|
feat(crm): add customer loyalty tier calculation
|
||||||
|
|
||||||
Implements three-tier loyalty system based on annual spend.
|
Implements three-tier loyalty system based on annual spend.
|
||||||
Includes migration for existing customer data.
|
Includes migration for existing customer data.
|
||||||
|
|
||||||
Refs #5234
|
Refs #5234
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
fix(oms): prevent duplicate return submissions
|
fix(oms): prevent duplicate return submissions
|
||||||
|
|
||||||
Adds debouncing to return form submission and validates
|
Adds debouncing to return form submission and validates
|
||||||
against existing returns in the last 60 seconds.
|
against existing returns in the last 60 seconds.
|
||||||
|
|
||||||
Closes #5891
|
Closes #5891
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
refactor(catalogue): extract product search into dedicated service
|
refactor(catalogue): extract product search into dedicated service
|
||||||
|
|
||||||
Moves search logic from component to ProductSearchService
|
Moves search logic from component to ProductSearchService
|
||||||
for better testability and reusability.
|
for better testability and reusability.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
perf(remission): optimize remission list query with pagination
|
perf(remission): optimize remission list query with pagination
|
||||||
|
|
||||||
Reduces initial load time from 3s to 800ms by implementing
|
Reduces initial load time from 3s to 800ms by implementing
|
||||||
cursor-based pagination.
|
cursor-based pagination.
|
||||||
|
|
||||||
Closes #6234
|
Closes #6234
|
||||||
```
|
```
|
||||||
|
|
||||||
### Bad Commit Messages
|
### Bad Commit Messages
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Too vague
|
# Too vague
|
||||||
fix: bug fixes
|
fix: bug fixes
|
||||||
|
|
||||||
# Missing scope
|
# Missing scope
|
||||||
feat: new feature
|
feat: new feature
|
||||||
|
|
||||||
# Wrong tense
|
# Wrong tense
|
||||||
fixed the login issue
|
fixed the login issue
|
||||||
|
|
||||||
# Including banned tags
|
# Including banned tags
|
||||||
feat(checkout): add feature
|
feat(checkout): add feature
|
||||||
|
|
||||||
Generated with Claude Code
|
Generated with Claude Code
|
||||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Git Configuration Checks
|
## Git Configuration Checks
|
||||||
|
|
||||||
### Verify Git Setup
|
### Verify Git Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check current branch
|
# Check current branch
|
||||||
git branch --show-current
|
git branch --show-current
|
||||||
|
|
||||||
# Verify remote
|
# Verify remote
|
||||||
git remote -v # Should show origin pointing to ISA-Frontend
|
git remote -v # Should show origin pointing to ISA-Frontend
|
||||||
|
|
||||||
# Check for uncommitted changes
|
# Check for uncommitted changes
|
||||||
git status
|
git status
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Mistakes to Avoid
|
## Common Mistakes to Avoid
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# ❌ Creating PR against main
|
# ❌ Creating PR against main
|
||||||
gh pr create --base main # WRONG
|
gh pr create --base main # WRONG
|
||||||
|
|
||||||
# ✅ Always target develop
|
# ✅ Always target develop
|
||||||
gh pr create --base develop # CORRECT
|
gh pr create --base develop # CORRECT
|
||||||
|
|
||||||
# ❌ Using underscores in branch names
|
# ❌ Using underscores in branch names
|
||||||
git checkout -b feature/5391_my_feature # WRONG
|
git checkout -b feature/5391_my_feature # WRONG
|
||||||
|
|
||||||
# ✅ Use hyphens
|
# ✅ Use hyphens
|
||||||
git checkout -b feature/5391-my-feature # CORRECT
|
git checkout -b feature/5391-my-feature # CORRECT
|
||||||
|
|
||||||
# ❌ Adding co-author tags
|
# ❌ Adding co-author tags
|
||||||
git commit -m "feat: something
|
git commit -m "feat: something
|
||||||
|
|
||||||
Co-Authored-By: Claude <noreply@anthropic.com>" # WRONG
|
Co-Authored-By: Claude <noreply@anthropic.com>" # WRONG
|
||||||
|
|
||||||
# ✅ Clean commit message
|
# ✅ Clean commit message
|
||||||
git commit -m "feat(scope): something" # CORRECT
|
git commit -m "feat(scope): something" # CORRECT
|
||||||
|
|
||||||
# ❌ Forgetting task ID in branch name
|
# ❌ Forgetting task ID in branch name
|
||||||
git checkout -b feature/new-checkout-flow # WRONG
|
git checkout -b feature/new-checkout-flow # WRONG
|
||||||
|
|
||||||
# ✅ Include task ID
|
# ✅ Include task ID
|
||||||
git checkout -b feature/5391-new-checkout-flow # CORRECT
|
git checkout -b feature/5391-new-checkout-flow # CORRECT
|
||||||
```
|
```
|
||||||
|
|
||||||
## Integration with Claude Code
|
## Integration with Claude Code
|
||||||
|
|
||||||
When Claude Code creates commits or PRs:
|
When Claude Code creates commits or PRs:
|
||||||
|
|
||||||
### Commit Creation
|
### Commit Creation
|
||||||
```bash
|
```bash
|
||||||
# Claude uses conventional commits WITHOUT attribution
|
# Claude uses conventional commits WITHOUT attribution
|
||||||
git commit -m "feat(checkout): implement bonus card selection
|
git commit -m "feat(checkout): implement bonus card selection
|
||||||
|
|
||||||
Adds logic for selecting primary bonus card during checkout
|
Adds logic for selecting primary bonus card during checkout
|
||||||
for delivery orders. Includes validation and error handling.
|
for delivery orders. Includes validation and error handling.
|
||||||
|
|
||||||
Refs #5391"
|
Refs #5391"
|
||||||
```
|
```
|
||||||
|
|
||||||
### PR Creation
|
### PR Creation
|
||||||
```bash
|
```bash
|
||||||
# Target develop by default
|
# Target develop by default
|
||||||
gh pr create --base develop \
|
gh pr create --base develop \
|
||||||
--title "feat(checkout): implement bonus card selection" \
|
--title "feat(checkout): implement bonus card selection" \
|
||||||
--body "## Summary
|
--body "## Summary
|
||||||
- Add primary bonus card selection logic
|
- Add primary bonus card selection logic
|
||||||
- Implement validation for delivery orders
|
- Implement validation for delivery orders
|
||||||
- Add error handling for API failures
|
- Add error handling for API failures
|
||||||
|
|
||||||
## Related Tasks
|
## Related Tasks
|
||||||
- Closes #5391
|
- Closes #5391
|
||||||
|
|
||||||
## Test Plan
|
## Test Plan
|
||||||
- [x] Unit tests added
|
- [x] Unit tests added
|
||||||
- [x] E2E attributes added
|
- [x] E2E attributes added
|
||||||
- [x] Manual testing completed"
|
- [x] Manual testing completed"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Branch Cleanup
|
## Branch Cleanup
|
||||||
|
|
||||||
### After PR Merge
|
### After PR Merge
|
||||||
```bash
|
```bash
|
||||||
# Update develop
|
# Update develop
|
||||||
git checkout develop
|
git checkout develop
|
||||||
git pull origin develop
|
git pull origin develop
|
||||||
|
|
||||||
# Delete local feature branch
|
# Delete local feature branch
|
||||||
git branch -d feature/5391-praemie-checkout
|
git branch -d feature/5391-praemie-checkout
|
||||||
|
|
||||||
# Delete remote branch (usually done by PR merge)
|
# Delete remote branch (usually done by PR merge)
|
||||||
git push origin --delete feature/5391-praemie-checkout
|
git push origin --delete feature/5391-praemie-checkout
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Branch naming
|
# Branch naming
|
||||||
feature/{task-id}-{description}
|
feature/{task-id}-{description}
|
||||||
bugfix/{task-id}-{description}
|
bugfix/{task-id}-{description}
|
||||||
hotfix/{task-id}-{description}
|
hotfix/{task-id}-{description}
|
||||||
|
|
||||||
# Commit format
|
# Commit format
|
||||||
<type>(<scope>): <description>
|
<type>(<scope>): <description>
|
||||||
|
|
||||||
# Common types
|
# Common types
|
||||||
feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||||
|
|
||||||
# PR target
|
# PR target
|
||||||
Always: develop (NOT main)
|
Always: develop (NOT main)
|
||||||
|
|
||||||
# Banned in commits
|
# Banned in commits
|
||||||
- "Generated with Claude Code"
|
- "Generated with Claude Code"
|
||||||
- "Co-Authored-By: Claude"
|
- "Co-Authored-By: Claude"
|
||||||
- Any AI attribution
|
- Any AI attribution
|
||||||
```
|
```
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- [Conventional Commits](https://www.conventionalcommits.org/)
|
- [Conventional Commits](https://www.conventionalcommits.org/)
|
||||||
- Project PR template: `.github/pull_request_template.md`
|
- Project PR template: `.github/pull_request_template.md`
|
||||||
- Code review standards: `.github/review-instructions.md`
|
- Code review standards: `.github/review-instructions.md`
|
||||||
|
|||||||
@@ -1,284 +1,277 @@
|
|||||||
---
|
---
|
||||||
name: library-creator
|
name: library-creator
|
||||||
description: This skill should be used when creating feature/data-access/ui/util libraries or user says "create library", "new library", "scaffold library". Creates new Angular libraries in ISA-Frontend monorepo with proper Nx configuration, Vitest setup, architectural tags, and path aliases.
|
description: This skill should be used when creating feature/data-access/ui/util libraries or user says "create library", "new library", "scaffold library". Creates new Angular libraries in ISA-Frontend monorepo with proper Nx configuration, Vitest setup, architectural tags, and path aliases.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Library Creator
|
# Library Creator
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Automate the creation of new Angular libraries following ISA-Frontend conventions. This skill handles the complete scaffolding workflow including Nx generation, Vitest configuration with CI/CD integration, architectural tags, path alias verification, and initial validation.
|
Automate the creation of new Angular libraries following ISA-Frontend conventions. This skill handles the complete scaffolding workflow including Nx generation, Vitest configuration with CI/CD integration, architectural tags, path alias verification, and initial validation.
|
||||||
|
|
||||||
## When to Use This Skill
|
## Required Parameters
|
||||||
|
|
||||||
Invoke this skill when:
|
Collect these parameters from the user:
|
||||||
- User requests creating a new library
|
- **domain**: Domain name (oms, remission, checkout, ui, core, shared, utils)
|
||||||
- User mentions "new library", "scaffold library", or "create feature"
|
- **layer**: Layer type (feature, data-access, ui, util)
|
||||||
- User wants to add a new domain/layer/feature to the monorepo
|
- **name**: Library name in kebab-case
|
||||||
|
|
||||||
## Required Parameters
|
## Library Creation Workflow
|
||||||
|
|
||||||
Collect these parameters from the user:
|
### Step 1: Validate Input
|
||||||
- **domain**: Domain name (oms, remission, checkout, ui, core, shared, utils)
|
|
||||||
- **layer**: Layer type (feature, data-access, ui, util)
|
**Verify Domain:**
|
||||||
- **name**: Library name in kebab-case
|
- Use `docs-researcher` to check `docs/library-reference.md`
|
||||||
|
- Ensure domain follows existing patterns
|
||||||
## Library Creation Workflow
|
|
||||||
|
**Validate Layer:**
|
||||||
### Step 1: Validate Input
|
- Must be one of: feature, data-access, ui, util
|
||||||
|
|
||||||
**Verify Domain:**
|
**Check Name:**
|
||||||
- Use `docs-researcher` to check `docs/library-reference.md`
|
- Must be kebab-case
|
||||||
- Ensure domain follows existing patterns
|
- Must not conflict with existing libraries
|
||||||
|
|
||||||
**Validate Layer:**
|
**Determine Path Depth:**
|
||||||
- Must be one of: feature, data-access, ui, util
|
- 3 levels: `libs/domain/layer/name` → `../../../`
|
||||||
|
- 4 levels: `libs/domain/type/layer/name` → `../../../../`
|
||||||
**Check Name:**
|
|
||||||
- Must be kebab-case
|
### Step 2: Run Dry-Run
|
||||||
- Must not conflict with existing libraries
|
|
||||||
|
Execute Nx generator with `--dry-run` to preview changes:
|
||||||
**Determine Path Depth:**
|
|
||||||
- 3 levels: `libs/domain/layer/name` → `../../../`
|
```bash
|
||||||
- 4 levels: `libs/domain/type/layer/name` → `../../../../`
|
npx nx generate @nx/angular:library \
|
||||||
|
--name=[domain]-[layer]-[name] \
|
||||||
### Step 2: Run Dry-Run
|
--directory=libs/[domain]/[layer]/[name] \
|
||||||
|
--importPath=@isa/[domain]/[layer]/[name] \
|
||||||
Execute Nx generator with `--dry-run` to preview changes:
|
--prefix=[domain] \
|
||||||
|
--style=css \
|
||||||
```bash
|
--unitTestRunner=vitest \
|
||||||
npx nx generate @nx/angular:library \
|
--standalone=true \
|
||||||
--name=[domain]-[layer]-[name] \
|
--skipTests=false \
|
||||||
--directory=libs/[domain]/[layer]/[name] \
|
--dry-run
|
||||||
--importPath=@isa/[domain]/[layer]/[name] \
|
```
|
||||||
--prefix=[domain] \
|
|
||||||
--style=css \
|
Review output with user before proceeding.
|
||||||
--unitTestRunner=vitest \
|
|
||||||
--standalone=true \
|
### Step 3: Generate Library
|
||||||
--skipTests=false \
|
|
||||||
--dry-run
|
Execute without `--dry-run`:
|
||||||
```
|
|
||||||
|
```bash
|
||||||
Review output with user before proceeding.
|
npx nx generate @nx/angular:library \
|
||||||
|
--name=[domain]-[layer]-[name] \
|
||||||
### Step 3: Generate Library
|
--directory=libs/[domain]/[layer]/[name] \
|
||||||
|
--importPath=@isa/[domain]/[layer]/[name] \
|
||||||
Execute without `--dry-run`:
|
--prefix=[domain] \
|
||||||
|
--style=css \
|
||||||
```bash
|
--unitTestRunner=vitest \
|
||||||
npx nx generate @nx/angular:library \
|
--standalone=true \
|
||||||
--name=[domain]-[layer]-[name] \
|
--skipTests=false
|
||||||
--directory=libs/[domain]/[layer]/[name] \
|
```
|
||||||
--importPath=@isa/[domain]/[layer]/[name] \
|
|
||||||
--prefix=[domain] \
|
### Step 4: Add Architectural Tags
|
||||||
--style=css \
|
|
||||||
--unitTestRunner=vitest \
|
**CRITICAL**: Immediately after library generation, add proper tags to `project.json` for `@nx/enforce-module-boundaries`.
|
||||||
--standalone=true \
|
|
||||||
--skipTests=false
|
Run the tagging script:
|
||||||
```
|
```bash
|
||||||
|
node scripts/add-library-tags.js
|
||||||
### Step 4: Add Architectural Tags
|
```
|
||||||
|
|
||||||
**CRITICAL**: Immediately after library generation, add proper tags to `project.json` for `@nx/enforce-module-boundaries`.
|
Or manually add tags to `libs/[domain]/[layer]/[name]/project.json`:
|
||||||
|
|
||||||
Run the tagging script:
|
```json
|
||||||
```bash
|
{
|
||||||
node scripts/add-library-tags.js
|
"name": "[domain]-[layer]-[name]",
|
||||||
```
|
"tags": [
|
||||||
|
"scope:[domain]",
|
||||||
Or manually add tags to `libs/[domain]/[layer]/[name]/project.json`:
|
"type:[layer]"
|
||||||
|
]
|
||||||
```json
|
}
|
||||||
{
|
```
|
||||||
"name": "[domain]-[layer]-[name]",
|
|
||||||
"tags": [
|
**Tag Rules:**
|
||||||
"scope:[domain]",
|
- **Scope tag**: `scope:[domain]` (e.g., `scope:oms`, `scope:crm`, `scope:ui`, `scope:shared`)
|
||||||
"type:[layer]"
|
- **Type tag**: `type:[layer]` (e.g., `type:feature`, `type:data-access`, `type:ui`, `type:util`)
|
||||||
]
|
|
||||||
}
|
**Examples:**
|
||||||
```
|
- `libs/oms/feature/return-search` → `["scope:oms", "type:feature"]`
|
||||||
|
- `libs/ui/buttons` → `["scope:ui", "type:ui"]`
|
||||||
**Tag Rules:**
|
- `libs/shared/scanner` → `["scope:shared", "type:shared"]`
|
||||||
- **Scope tag**: `scope:[domain]` (e.g., `scope:oms`, `scope:crm`, `scope:ui`, `scope:shared`)
|
- `libs/core/auth` → `["scope:core", "type:core"]`
|
||||||
- **Type tag**: `type:[layer]` (e.g., `type:feature`, `type:data-access`, `type:ui`, `type:util`)
|
|
||||||
|
**Verification:**
|
||||||
**Examples:**
|
```bash
|
||||||
- `libs/oms/feature/return-search` → `["scope:oms", "type:feature"]`
|
# Check tags were added
|
||||||
- `libs/ui/buttons` → `["scope:ui", "type:ui"]`
|
cat libs/[domain]/[layer]/[name]/project.json | jq '.tags'
|
||||||
- `libs/shared/scanner` → `["scope:shared", "type:shared"]`
|
|
||||||
- `libs/core/auth` → `["scope:core", "type:core"]`
|
# Should output: ["scope:[domain]", "type:[layer]"]
|
||||||
|
```
|
||||||
**Verification:**
|
|
||||||
```bash
|
### Step 5: Configure Vitest with JUnit and Cobertura
|
||||||
# Check tags were added
|
|
||||||
cat libs/[domain]/[layer]/[name]/project.json | jq '.tags'
|
Update `libs/[path]/vite.config.mts` with this template:
|
||||||
|
|
||||||
# Should output: ["scope:[domain]", "type:[layer]"]
|
```typescript
|
||||||
```
|
/// <reference types='vitest' />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
### Step 5: Configure Vitest with JUnit and Cobertura
|
import angular from '@analogjs/vite-plugin-angular';
|
||||||
|
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||||
Update `libs/[path]/vite.config.mts` with this template:
|
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||||
|
|
||||||
```typescript
|
export default
|
||||||
/// <reference types='vitest' />
|
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
|
||||||
import { defineConfig } from 'vite';
|
defineConfig(() => ({
|
||||||
import angular from '@analogjs/vite-plugin-angular';
|
root: __dirname,
|
||||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
cacheDir: '../../../node_modules/.vite/libs/[path]',
|
||||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||||
|
test: {
|
||||||
export default
|
watch: false,
|
||||||
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
|
globals: true,
|
||||||
defineConfig(() => ({
|
environment: 'jsdom',
|
||||||
root: __dirname,
|
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
cacheDir: '../../../node_modules/.vite/libs/[path]',
|
setupFiles: ['src/test-setup.ts'],
|
||||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
reporters: [
|
||||||
test: {
|
'default',
|
||||||
watch: false,
|
['junit', { outputFile: '../../../testresults/junit-[library-name].xml' }],
|
||||||
globals: true,
|
],
|
||||||
environment: 'jsdom',
|
coverage: {
|
||||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
reportsDirectory: '../../../coverage/libs/[path]',
|
||||||
setupFiles: ['src/test-setup.ts'],
|
provider: 'v8' as const,
|
||||||
reporters: [
|
reporter: ['text', 'cobertura'],
|
||||||
'default',
|
},
|
||||||
['junit', { outputFile: '../../../testresults/junit-[library-name].xml' }],
|
},
|
||||||
],
|
}));
|
||||||
coverage: {
|
```
|
||||||
reportsDirectory: '../../../coverage/libs/[path]',
|
|
||||||
provider: 'v8' as const,
|
**Critical**: Adjust path depth (`../../../` or `../../../../`) based on library location.
|
||||||
reporter: ['text', 'cobertura'],
|
|
||||||
},
|
### Step 6: Verify Configuration
|
||||||
},
|
|
||||||
}));
|
**Check Path Alias:**
|
||||||
```
|
- Verify `tsconfig.base.json` was updated
|
||||||
|
- Should have: `"@isa/[domain]/[layer]/[name]": ["libs/[domain]/[layer]/[name]/src/index.ts"]`
|
||||||
**Critical**: Adjust path depth (`../../../` or `../../../../`) based on library location.
|
|
||||||
|
**Run Initial Test:**
|
||||||
### Step 6: Verify Configuration
|
```bash
|
||||||
|
npx nx test [library-name] --coverage.enabled=true --skip-nx-cache
|
||||||
**Check Path Alias:**
|
```
|
||||||
- Verify `tsconfig.base.json` was updated
|
|
||||||
- Should have: `"@isa/[domain]/[layer]/[name]": ["libs/[domain]/[layer]/[name]/src/index.ts"]`
|
**Verify CI/CD Files Created:**
|
||||||
|
- JUnit XML: `testresults/junit-[library-name].xml`
|
||||||
**Run Initial Test:**
|
- Cobertura XML: `coverage/libs/[path]/cobertura-coverage.xml`
|
||||||
```bash
|
|
||||||
npx nx test [library-name] --coverage.enabled=true --skip-nx-cache
|
### Step 7: Create Library README
|
||||||
```
|
|
||||||
|
Use `docs-researcher` to find similar library READMEs, then create comprehensive documentation including:
|
||||||
**Verify CI/CD Files Created:**
|
- Overview and purpose
|
||||||
- JUnit XML: `testresults/junit-[library-name].xml`
|
- Installation/import instructions
|
||||||
- Cobertura XML: `coverage/libs/[path]/cobertura-coverage.xml`
|
- API documentation
|
||||||
|
- Usage examples
|
||||||
### Step 7: Create Library README
|
- Testing information (Vitest + Angular Testing Utilities)
|
||||||
|
|
||||||
Use `docs-researcher` to find similar library READMEs, then create comprehensive documentation including:
|
### Step 8: Update Library Reference
|
||||||
- Overview and purpose
|
|
||||||
- Installation/import instructions
|
Add entry to `docs/library-reference.md` under appropriate domain:
|
||||||
- API documentation
|
|
||||||
- Usage examples
|
```markdown
|
||||||
- Testing information (Vitest + Angular Testing Utilities)
|
#### `@isa/[domain]/[layer]/[name]`
|
||||||
|
**Path:** `libs/[domain]/[layer]/[name]`
|
||||||
### Step 8: Update Library Reference
|
**Type:** [Feature/Data Access/UI/Util]
|
||||||
|
**Testing:** Vitest
|
||||||
Add entry to `docs/library-reference.md` under appropriate domain:
|
|
||||||
|
[Brief description]
|
||||||
```markdown
|
```
|
||||||
#### `@isa/[domain]/[layer]/[name]`
|
|
||||||
**Path:** `libs/[domain]/[layer]/[name]`
|
### Step 9: Run Full Validation
|
||||||
**Type:** [Feature/Data Access/UI/Util]
|
|
||||||
**Testing:** Vitest
|
Execute validation commands to ensure library is properly configured:
|
||||||
|
|
||||||
[Brief description]
|
```bash
|
||||||
```
|
# Lint (includes boundary checks)
|
||||||
|
npx nx lint [library-name]
|
||||||
### Step 9: Run Full Validation
|
|
||||||
|
# Test with coverage
|
||||||
Execute validation commands to ensure library is properly configured:
|
npx nx test [library-name] --coverage.enabled=true --skip-nx-cache
|
||||||
|
|
||||||
```bash
|
# Build (if buildable)
|
||||||
# Lint (includes boundary checks)
|
npx nx build [library-name]
|
||||||
npx nx lint [library-name]
|
|
||||||
|
# Dependency graph
|
||||||
# Test with coverage
|
npx nx graph --focus=[library-name]
|
||||||
npx nx test [library-name] --coverage.enabled=true --skip-nx-cache
|
```
|
||||||
|
|
||||||
# Build (if buildable)
|
### Step 10: Generate Creation Report
|
||||||
npx nx build [library-name]
|
|
||||||
|
Provide this structured report to the user:
|
||||||
# Dependency graph
|
|
||||||
npx nx graph --focus=[library-name]
|
```
|
||||||
```
|
Library Created Successfully
|
||||||
|
============================
|
||||||
### Step 10: Generate Creation Report
|
|
||||||
|
Library Name: [domain]-[layer]-[name]
|
||||||
Provide this structured report to the user:
|
Path: libs/[domain]/[layer]/[name]
|
||||||
|
Import Alias: @isa/[domain]/[layer]/[name]
|
||||||
```
|
|
||||||
Library Created Successfully
|
✅ Configuration
|
||||||
============================
|
----------------
|
||||||
|
Test Framework: Vitest with Angular Testing Utilities
|
||||||
Library Name: [domain]-[layer]-[name]
|
Style: CSS
|
||||||
Path: libs/[domain]/[layer]/[name]
|
Standalone: Yes
|
||||||
Import Alias: @isa/[domain]/[layer]/[name]
|
Tags: scope:[domain], type:[layer]
|
||||||
|
JUnit Reporter: ✅ testresults/junit-[library-name].xml
|
||||||
✅ Configuration
|
Cobertura Coverage: ✅ coverage/libs/[path]/cobertura-coverage.xml
|
||||||
----------------
|
|
||||||
Test Framework: Vitest with Angular Testing Utilities
|
📦 Import Statement
|
||||||
Style: CSS
|
-------------------
|
||||||
Standalone: Yes
|
import { Component } from '@isa/[domain]/[layer]/[name]';
|
||||||
Tags: scope:[domain], type:[layer]
|
|
||||||
JUnit Reporter: ✅ testresults/junit-[library-name].xml
|
🧪 Test Commands
|
||||||
Cobertura Coverage: ✅ coverage/libs/[path]/cobertura-coverage.xml
|
----------------
|
||||||
|
npx nx test [library-name] --skip-nx-cache
|
||||||
📦 Import Statement
|
npx nx test [library-name] --coverage.enabled=true --skip-nx-cache
|
||||||
-------------------
|
|
||||||
import { Component } from '@isa/[domain]/[layer]/[name]';
|
🏗️ Architecture Compliance
|
||||||
|
--------------------------
|
||||||
🧪 Test Commands
|
Tags enforce module boundaries via @nx/enforce-module-boundaries
|
||||||
----------------
|
Run lint to check for violations: npx nx lint [library-name]
|
||||||
npx nx test [library-name] --skip-nx-cache
|
|
||||||
npx nx test [library-name] --coverage.enabled=true --skip-nx-cache
|
📝 Next Steps
|
||||||
|
-------------
|
||||||
🏗️ Architecture Compliance
|
1. Develop library features
|
||||||
--------------------------
|
2. Write tests using Vitest + Angular Testing Utilities
|
||||||
Tags enforce module boundaries via @nx/enforce-module-boundaries
|
3. Add E2E attributes (data-what, data-which) to templates
|
||||||
Run lint to check for violations: npx nx lint [library-name]
|
4. Update README with usage examples
|
||||||
|
5. Follow architecture rules (see eslint.config.js for constraints)
|
||||||
📝 Next Steps
|
```
|
||||||
-------------
|
|
||||||
1. Develop library features
|
## Error Handling
|
||||||
2. Write tests using Vitest + Angular Testing Utilities
|
|
||||||
3. Add E2E attributes (data-what, data-which) to templates
|
**Path depth mismatch:**
|
||||||
4. Update README with usage examples
|
- Count directory levels from workspace root
|
||||||
5. Follow architecture rules (see eslint.config.js for constraints)
|
- Adjust `../` in outputFile and reportsDirectory
|
||||||
```
|
|
||||||
|
**TypeScript errors in vite.config.mts:**
|
||||||
## Error Handling
|
- Add `// @ts-expect-error` before `defineConfig()`
|
||||||
|
|
||||||
**Path depth mismatch:**
|
**Path alias not working:**
|
||||||
- Count directory levels from workspace root
|
- Check tsconfig.base.json
|
||||||
- Adjust `../` in outputFile and reportsDirectory
|
- Run `npx nx reset`
|
||||||
|
- Restart TypeScript server
|
||||||
**TypeScript errors in vite.config.mts:**
|
|
||||||
- Add `// @ts-expect-error` before `defineConfig()`
|
**Library already exists:**
|
||||||
|
- Check `tsconfig.base.json` for existing path alias
|
||||||
**Path alias not working:**
|
- Use Grep to search for existing library name
|
||||||
- Check tsconfig.base.json
|
- Suggest alternative name to user
|
||||||
- Run `npx nx reset`
|
|
||||||
- Restart TypeScript server
|
## References
|
||||||
|
|
||||||
**Library already exists:**
|
- docs/guidelines/testing.md (Vitest, JUnit, Cobertura sections)
|
||||||
- Check `tsconfig.base.json` for existing path alias
|
- docs/library-reference.md (domain patterns)
|
||||||
- Use Grep to search for existing library name
|
- CLAUDE.md (Library Organization, Testing Framework sections)
|
||||||
- Suggest alternative name to user
|
- eslint.config.js (@nx/enforce-module-boundaries configuration)
|
||||||
|
- scripts/add-library-tags.js (automatic tagging script)
|
||||||
## References
|
- .claude/skills/architecture-validator (boundary validation)
|
||||||
|
- Nx Angular Library Generator: https://nx.dev/nx-api/angular/generators/library
|
||||||
- docs/guidelines/testing.md (Vitest, JUnit, Cobertura sections)
|
- Nx Enforce Module Boundaries: https://nx.dev/nx-api/eslint-plugin/documents/enforce-module-boundaries
|
||||||
- docs/library-reference.md (domain patterns)
|
|
||||||
- CLAUDE.md (Library Organization, Testing Framework sections)
|
|
||||||
- eslint.config.js (@nx/enforce-module-boundaries configuration)
|
|
||||||
- scripts/add-library-tags.js (automatic tagging script)
|
|
||||||
- .claude/skills/architecture-enforcer (boundary validation)
|
|
||||||
- Nx Angular Library Generator: https://nx.dev/nx-api/angular/generators/library
|
|
||||||
- Nx Enforce Module Boundaries: https://nx.dev/nx-api/eslint-plugin/documents/enforce-module-boundaries
|
|
||||||
|
|||||||
@@ -1,272 +1,234 @@
|
|||||||
---
|
---
|
||||||
name: logging
|
name: logging
|
||||||
description: This skill should be used when working with Angular components, directives, services, pipes, guards, or TypeScript classes. Logging is MANDATORY in all Angular files. Implements @isa/core/logging with logger() factory pattern, appropriate log levels, lazy evaluation for performance, error handling, and avoids console.log and common mistakes.
|
description: This skill should be used when working with Angular components, directives, services, pipes, guards, or TypeScript classes. Logging is MANDATORY in all Angular files. Implements @isa/core/logging with logger() factory pattern, appropriate log levels, lazy evaluation for performance, error handling, and avoids console.log and common mistakes.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Logging Helper Skill
|
# Logging
|
||||||
|
|
||||||
Ensures consistent and efficient logging using `@isa/core/logging` library.
|
Ensures consistent and efficient logging using `@isa/core/logging` library.
|
||||||
|
|
||||||
## When to Use
|
## Core Principles
|
||||||
|
|
||||||
- Adding logging to new components/services
|
### 1. Always Use Factory Pattern
|
||||||
- Refactoring existing logging code
|
|
||||||
- Reviewing code for proper logging patterns
|
```typescript
|
||||||
- Debugging logging issues
|
import { logger } from '@isa/core/logging';
|
||||||
|
|
||||||
## Core Principles
|
// ✅ DO
|
||||||
|
#logger = logger();
|
||||||
### 1. Always Use Factory Pattern
|
|
||||||
|
// ❌ DON'T
|
||||||
```typescript
|
constructor(private loggingService: LoggingService) {}
|
||||||
import { logger } from '@isa/core/logging';
|
```
|
||||||
|
|
||||||
// ✅ DO
|
### 2. Choose Appropriate Log Levels
|
||||||
#logger = logger();
|
|
||||||
|
- **Trace**: Fine-grained debugging (method entry/exit)
|
||||||
// ❌ DON'T
|
- **Debug**: Development debugging (variable states)
|
||||||
constructor(private loggingService: LoggingService) {}
|
- **Info**: Runtime information (user actions, events)
|
||||||
```
|
- **Warn**: Potentially harmful situations
|
||||||
|
- **Error**: Errors affecting functionality
|
||||||
### 2. Choose Appropriate Log Levels
|
|
||||||
|
### 3. Context Patterns
|
||||||
- **Trace**: Fine-grained debugging (method entry/exit)
|
|
||||||
- **Debug**: Development debugging (variable states)
|
**Static Context** (component level):
|
||||||
- **Info**: Runtime information (user actions, events)
|
```typescript
|
||||||
- **Warn**: Potentially harmful situations
|
#logger = logger({ component: 'UserProfileComponent' });
|
||||||
- **Error**: Errors affecting functionality
|
```
|
||||||
|
|
||||||
### 3. Context Patterns
|
**Dynamic Context** (instance level):
|
||||||
|
```typescript
|
||||||
**Static Context** (component level):
|
#logger = logger(() => ({
|
||||||
```typescript
|
userId: this.authService.currentUserId,
|
||||||
#logger = logger({ component: 'UserProfileComponent' });
|
storeId: this.config.storeId
|
||||||
```
|
}));
|
||||||
|
```
|
||||||
**Dynamic Context** (instance level):
|
|
||||||
```typescript
|
**Message Context** (use functions for performance):
|
||||||
#logger = logger(() => ({
|
```typescript
|
||||||
userId: this.authService.currentUserId,
|
// ✅ Recommended - lazy evaluation
|
||||||
storeId: this.config.storeId
|
this.#logger.info('Order processed', () => ({
|
||||||
}));
|
orderId: order.id,
|
||||||
```
|
total: order.total,
|
||||||
|
timestamp: Date.now()
|
||||||
**Message Context** (use functions for performance):
|
}));
|
||||||
```typescript
|
|
||||||
// ✅ Recommended - lazy evaluation
|
// ✅ Acceptable - static values
|
||||||
this.#logger.info('Order processed', () => ({
|
this.#logger.info('Order processed', {
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
total: order.total,
|
status: 'completed'
|
||||||
timestamp: Date.now()
|
});
|
||||||
}));
|
```
|
||||||
|
|
||||||
// ✅ Acceptable - static values
|
## Essential Patterns
|
||||||
this.#logger.info('Order processed', {
|
|
||||||
orderId: order.id,
|
### Component Logging
|
||||||
status: 'completed'
|
```typescript
|
||||||
});
|
@Component({
|
||||||
```
|
selector: 'app-product-list',
|
||||||
|
standalone: true,
|
||||||
## Essential Patterns
|
})
|
||||||
|
export class ProductListComponent {
|
||||||
### Component Logging
|
#logger = logger({ component: 'ProductListComponent' });
|
||||||
```typescript
|
|
||||||
@Component({
|
ngOnInit(): void {
|
||||||
selector: 'app-product-list',
|
this.#logger.info('Component initialized');
|
||||||
standalone: true,
|
}
|
||||||
})
|
|
||||||
export class ProductListComponent {
|
onAction(id: string): void {
|
||||||
#logger = logger({ component: 'ProductListComponent' });
|
this.#logger.debug('Action triggered', { id });
|
||||||
|
}
|
||||||
ngOnInit(): void {
|
}
|
||||||
this.#logger.info('Component initialized');
|
```
|
||||||
}
|
|
||||||
|
### Service Logging
|
||||||
onAction(id: string): void {
|
```typescript
|
||||||
this.#logger.debug('Action triggered', { id });
|
@Injectable({ providedIn: 'root' })
|
||||||
}
|
export class DataService {
|
||||||
}
|
#logger = logger({ service: 'DataService' });
|
||||||
```
|
|
||||||
|
fetchData(endpoint: string): Observable<Data> {
|
||||||
### Service Logging
|
this.#logger.debug('Fetching data', { endpoint });
|
||||||
```typescript
|
|
||||||
@Injectable({ providedIn: 'root' })
|
return this.http.get<Data>(endpoint).pipe(
|
||||||
export class DataService {
|
tap((data) => this.#logger.info('Data fetched', () => ({
|
||||||
#logger = logger({ service: 'DataService' });
|
endpoint,
|
||||||
|
size: data.length
|
||||||
fetchData(endpoint: string): Observable<Data> {
|
}))),
|
||||||
this.#logger.debug('Fetching data', { endpoint });
|
catchError((error) => {
|
||||||
|
this.#logger.error('Fetch failed', error, () => ({
|
||||||
return this.http.get<Data>(endpoint).pipe(
|
endpoint,
|
||||||
tap((data) => this.#logger.info('Data fetched', () => ({
|
status: error.status
|
||||||
endpoint,
|
}));
|
||||||
size: data.length
|
return throwError(() => error);
|
||||||
}))),
|
})
|
||||||
catchError((error) => {
|
);
|
||||||
this.#logger.error('Fetch failed', error, () => ({
|
}
|
||||||
endpoint,
|
}
|
||||||
status: error.status
|
```
|
||||||
}));
|
|
||||||
return throwError(() => error);
|
### Error Handling
|
||||||
})
|
```typescript
|
||||||
);
|
try {
|
||||||
}
|
await this.processOrder(orderId);
|
||||||
}
|
} catch (error) {
|
||||||
```
|
this.#logger.error('Order processing failed', error as Error, () => ({
|
||||||
|
orderId,
|
||||||
### Error Handling
|
step: this.currentStep,
|
||||||
```typescript
|
attemptNumber: this.retryCount
|
||||||
try {
|
}));
|
||||||
await this.processOrder(orderId);
|
throw error;
|
||||||
} catch (error) {
|
}
|
||||||
this.#logger.error('Order processing failed', error as Error, () => ({
|
```
|
||||||
orderId,
|
|
||||||
step: this.currentStep,
|
### Hierarchical Context
|
||||||
attemptNumber: this.retryCount
|
```typescript
|
||||||
}));
|
@Component({
|
||||||
throw error;
|
providers: [
|
||||||
}
|
provideLoggerContext({ feature: 'checkout', module: 'sales' })
|
||||||
```
|
]
|
||||||
|
})
|
||||||
### Hierarchical Context
|
export class CheckoutComponent {
|
||||||
```typescript
|
#logger = logger(() => ({ userId: this.userService.currentUserId }));
|
||||||
@Component({
|
|
||||||
providers: [
|
// Logs include: feature, module, userId + message context
|
||||||
provideLoggerContext({ feature: 'checkout', module: 'sales' })
|
}
|
||||||
]
|
```
|
||||||
})
|
|
||||||
export class CheckoutComponent {
|
## Common Mistakes to Avoid
|
||||||
#logger = logger(() => ({ userId: this.userService.currentUserId }));
|
|
||||||
|
```typescript
|
||||||
// Logs include: feature, module, userId + message context
|
// ❌ Don't use console.log
|
||||||
}
|
console.log('User logged in');
|
||||||
```
|
// ✅ Use logger
|
||||||
|
this.#logger.info('User logged in');
|
||||||
## Common Mistakes to Avoid
|
|
||||||
|
// ❌ Don't create expensive context eagerly
|
||||||
```typescript
|
this.#logger.debug('Processing', {
|
||||||
// ❌ Don't use console.log
|
data: this.computeExpensive() // Always executes
|
||||||
console.log('User logged in');
|
});
|
||||||
// ✅ Use logger
|
// ✅ Use function for lazy evaluation
|
||||||
this.#logger.info('User logged in');
|
this.#logger.debug('Processing', () => ({
|
||||||
|
data: this.computeExpensive() // Only if debug enabled
|
||||||
// ❌ Don't create expensive context eagerly
|
}));
|
||||||
this.#logger.debug('Processing', {
|
|
||||||
data: this.computeExpensive() // Always executes
|
// ❌ Don't log in tight loops
|
||||||
});
|
for (const item of items) {
|
||||||
// ✅ Use function for lazy evaluation
|
this.#logger.debug('Item', { item });
|
||||||
this.#logger.debug('Processing', () => ({
|
}
|
||||||
data: this.computeExpensive() // Only if debug enabled
|
// ✅ Log aggregates
|
||||||
}));
|
this.#logger.debug('Batch processed', () => ({
|
||||||
|
count: items.length
|
||||||
// ❌ Don't log in tight loops
|
}));
|
||||||
for (const item of items) {
|
|
||||||
this.#logger.debug('Item', { item });
|
// ❌ Don't log sensitive data
|
||||||
}
|
this.#logger.info('User auth', { password: user.password });
|
||||||
// ✅ Log aggregates
|
// ✅ Log safe identifiers only
|
||||||
this.#logger.debug('Batch processed', () => ({
|
this.#logger.info('User auth', { userId: user.id });
|
||||||
count: items.length
|
|
||||||
}));
|
// ❌ Don't miss error object
|
||||||
|
this.#logger.error('Failed');
|
||||||
// ❌ Don't log sensitive data
|
// ✅ Include error object
|
||||||
this.#logger.info('User auth', { password: user.password });
|
this.#logger.error('Failed', error as Error);
|
||||||
// ✅ Log safe identifiers only
|
```
|
||||||
this.#logger.info('User auth', { userId: user.id });
|
|
||||||
|
## Configuration
|
||||||
// ❌ Don't miss error object
|
|
||||||
this.#logger.error('Failed');
|
### App Configuration
|
||||||
// ✅ Include error object
|
```typescript
|
||||||
this.#logger.error('Failed', error as Error);
|
// app.config.ts
|
||||||
```
|
import { ApplicationConfig, isDevMode } from '@angular/core';
|
||||||
|
import {
|
||||||
## Configuration
|
provideLogging, withLogLevel, withSink,
|
||||||
|
LogLevel, ConsoleLogSink
|
||||||
### App Configuration
|
} from '@isa/core/logging';
|
||||||
```typescript
|
|
||||||
// app.config.ts
|
export const appConfig: ApplicationConfig = {
|
||||||
import { ApplicationConfig, isDevMode } from '@angular/core';
|
providers: [
|
||||||
import {
|
provideLogging(
|
||||||
provideLogging, withLogLevel, withSink,
|
withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn),
|
||||||
LogLevel, ConsoleLogSink
|
withSink(ConsoleLogSink),
|
||||||
} from '@isa/core/logging';
|
withContext({ app: 'ISA', version: '1.0.0' })
|
||||||
|
)
|
||||||
export const appConfig: ApplicationConfig = {
|
]
|
||||||
providers: [
|
};
|
||||||
provideLogging(
|
```
|
||||||
withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn),
|
|
||||||
withSink(ConsoleLogSink),
|
## Testing
|
||||||
withContext({ app: 'ISA', version: '1.0.0' })
|
|
||||||
)
|
```typescript
|
||||||
]
|
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||||
};
|
import { LoggingService } from '@isa/core/logging';
|
||||||
```
|
|
||||||
|
describe('MyComponent', () => {
|
||||||
## Testing
|
const createComponent = createComponentFactory({
|
||||||
|
component: MyComponent,
|
||||||
```typescript
|
mocks: [LoggingService]
|
||||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
});
|
||||||
import { LoggingService } from '@isa/core/logging';
|
|
||||||
|
it('should log error', () => {
|
||||||
describe('MyComponent', () => {
|
const spectator = createComponent();
|
||||||
const createComponent = createComponentFactory({
|
const loggingService = spectator.inject(LoggingService);
|
||||||
component: MyComponent,
|
|
||||||
mocks: [LoggingService]
|
spectator.component.riskyOperation();
|
||||||
});
|
|
||||||
|
expect(loggingService.error).toHaveBeenCalledWith(
|
||||||
it('should log error', () => {
|
expect.any(String),
|
||||||
const spectator = createComponent();
|
expect.any(Error),
|
||||||
const loggingService = spectator.inject(LoggingService);
|
expect.any(Function)
|
||||||
|
);
|
||||||
spectator.component.riskyOperation();
|
});
|
||||||
|
});
|
||||||
expect(loggingService.error).toHaveBeenCalledWith(
|
```
|
||||||
expect.any(String),
|
|
||||||
expect.any(Error),
|
## Additional Resources
|
||||||
expect.any(Function)
|
|
||||||
);
|
**For more detailed information:**
|
||||||
});
|
|
||||||
});
|
- **API signatures and patterns**: See [references/api-reference.md](references/api-reference.md) for complete API documentation
|
||||||
```
|
- **Real-world examples**: See [references/examples.md](references/examples.md) for components, services, guards, interceptors, and more
|
||||||
|
- **Troubleshooting**: See [references/troubleshooting.md](references/troubleshooting.md) for common issues and solutions
|
||||||
## Code Review Checklist
|
|
||||||
|
**Project documentation:**
|
||||||
- [ ] Uses `logger()` factory, not `LoggingService` injection
|
|
||||||
- [ ] Appropriate log level for each message
|
- Full library documentation: `libs/core/logging/README.md`
|
||||||
- [ ] Context functions for expensive operations
|
|
||||||
- [ ] No sensitive information (passwords, tokens, PII)
|
|
||||||
- [ ] No `console.log` statements
|
|
||||||
- [ ] Error logs include error object
|
|
||||||
- [ ] No logging in tight loops
|
|
||||||
- [ ] Component/service identified in context
|
|
||||||
- [ ] E2E attributes on interactive elements
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Import
|
|
||||||
import { logger, provideLoggerContext } from '@isa/core/logging';
|
|
||||||
|
|
||||||
// Create logger
|
|
||||||
#logger = logger(); // Basic
|
|
||||||
#logger = logger({ component: 'Name' }); // Static context
|
|
||||||
#logger = logger(() => ({ id: this.id })); // Dynamic context
|
|
||||||
|
|
||||||
// Log messages
|
|
||||||
this.#logger.trace('Detailed trace');
|
|
||||||
this.#logger.debug('Debug info');
|
|
||||||
this.#logger.info('General info', () => ({ key: value }));
|
|
||||||
this.#logger.warn('Warning');
|
|
||||||
this.#logger.error('Error', error, () => ({ context }));
|
|
||||||
|
|
||||||
// Component context
|
|
||||||
@Component({
|
|
||||||
providers: [provideLoggerContext({ feature: 'users' })]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Additional Resources
|
|
||||||
|
|
||||||
- Full documentation: `libs/core/logging/README.md`
|
|
||||||
- Examples: `.claude/skills/logging-helper/examples.md`
|
|
||||||
- Quick reference: `.claude/skills/logging-helper/reference.md`
|
|
||||||
- Troubleshooting: `.claude/skills/logging-helper/troubleshooting.md`
|
|
||||||
|
|||||||
@@ -1,192 +1,192 @@
|
|||||||
# Logging Quick Reference
|
# Logging API Reference
|
||||||
|
|
||||||
## API Signatures
|
## API Signatures
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Factory
|
// Factory
|
||||||
function logger(ctx?: MaybeLoggerContextFn): LoggerApi
|
function logger(ctx?: MaybeLoggerContextFn): LoggerApi
|
||||||
|
|
||||||
// Logger API
|
// Logger API
|
||||||
interface LoggerApi {
|
interface LoggerApi {
|
||||||
trace(message: string, context?: MaybeLoggerContextFn): void;
|
trace(message: string, context?: MaybeLoggerContextFn): void;
|
||||||
debug(message: string, context?: MaybeLoggerContextFn): void;
|
debug(message: string, context?: MaybeLoggerContextFn): void;
|
||||||
info(message: string, context?: MaybeLoggerContextFn): void;
|
info(message: string, context?: MaybeLoggerContextFn): void;
|
||||||
warn(message: string, context?: MaybeLoggerContextFn): void;
|
warn(message: string, context?: MaybeLoggerContextFn): void;
|
||||||
error(message: string, error?: Error, context?: MaybeLoggerContextFn): void;
|
error(message: string, error?: Error, context?: MaybeLoggerContextFn): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
type MaybeLoggerContextFn = LoggerContext | (() => LoggerContext);
|
type MaybeLoggerContextFn = LoggerContext | (() => LoggerContext);
|
||||||
interface LoggerContext { [key: string]: unknown; }
|
interface LoggerContext { [key: string]: unknown; }
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
| Pattern | Code |
|
| Pattern | Code |
|
||||||
|---------|------|
|
|---------|------|
|
||||||
| Basic logger | `#logger = logger()` |
|
| Basic logger | `#logger = logger()` |
|
||||||
| Static context | `#logger = logger({ component: 'Name' })` |
|
| Static context | `#logger = logger({ component: 'Name' })` |
|
||||||
| Dynamic context | `#logger = logger(() => ({ id: this.id }))` |
|
| Dynamic context | `#logger = logger(() => ({ id: this.id }))` |
|
||||||
| Log info | `this.#logger.info('Message')` |
|
| Log info | `this.#logger.info('Message')` |
|
||||||
| Log with context | `this.#logger.info('Message', () => ({ key: value }))` |
|
| Log with context | `this.#logger.info('Message', () => ({ key: value }))` |
|
||||||
| Log error | `this.#logger.error('Error', error)` |
|
| Log error | `this.#logger.error('Error', error)` |
|
||||||
| Error with context | `this.#logger.error('Error', error, () => ({ id }))` |
|
| Error with context | `this.#logger.error('Error', error, () => ({ id }))` |
|
||||||
| Component context | `providers: [provideLoggerContext({ feature: 'x' })]` |
|
| Component context | `providers: [provideLoggerContext({ feature: 'x' })]` |
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// app.config.ts
|
// app.config.ts
|
||||||
import { provideLogging, withLogLevel, withSink, withContext,
|
import { provideLogging, withLogLevel, withSink, withContext,
|
||||||
LogLevel, ConsoleLogSink } from '@isa/core/logging';
|
LogLevel, ConsoleLogSink } from '@isa/core/logging';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideLogging(
|
provideLogging(
|
||||||
withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn),
|
withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn),
|
||||||
withSink(ConsoleLogSink),
|
withSink(ConsoleLogSink),
|
||||||
withContext({ app: 'ISA', version: '1.0.0' })
|
withContext({ app: 'ISA', version: '1.0.0' })
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## Log Levels
|
## Log Levels
|
||||||
|
|
||||||
| Level | Use Case | Example |
|
| Level | Use Case | Example |
|
||||||
|-------|----------|---------|
|
|-------|----------|---------|
|
||||||
| `Trace` | Method entry/exit | `this.#logger.trace('Entering processData')` |
|
| `Trace` | Method entry/exit | `this.#logger.trace('Entering processData')` |
|
||||||
| `Debug` | Development info | `this.#logger.debug('Variable state', () => ({ x }))` |
|
| `Debug` | Development info | `this.#logger.debug('Variable state', () => ({ x }))` |
|
||||||
| `Info` | Runtime events | `this.#logger.info('User logged in', { userId })` |
|
| `Info` | Runtime events | `this.#logger.info('User logged in', { userId })` |
|
||||||
| `Warn` | Warnings | `this.#logger.warn('Deprecated API used')` |
|
| `Warn` | Warnings | `this.#logger.warn('Deprecated API used')` |
|
||||||
| `Error` | Errors | `this.#logger.error('Operation failed', error)` |
|
| `Error` | Errors | `this.#logger.error('Operation failed', error)` |
|
||||||
| `Off` | Disable logging | `withLogLevel(LogLevel.Off)` |
|
| `Off` | Disable logging | `withLogLevel(LogLevel.Off)` |
|
||||||
|
|
||||||
## Decision Trees
|
## Decision Trees
|
||||||
|
|
||||||
### Context Type Decision
|
### Context Type Decision
|
||||||
```
|
```
|
||||||
Value changes at runtime?
|
Value changes at runtime?
|
||||||
├─ Yes → () => ({ value: this.getValue() })
|
├─ Yes → () => ({ value: this.getValue() })
|
||||||
└─ No → { value: 'static' }
|
└─ No → { value: 'static' }
|
||||||
|
|
||||||
Computing value is expensive?
|
Computing value is expensive?
|
||||||
├─ Yes → () => ({ data: this.compute() })
|
├─ Yes → () => ({ data: this.compute() })
|
||||||
└─ No → Either works
|
└─ No → Either works
|
||||||
```
|
```
|
||||||
|
|
||||||
### Log Level Decision
|
### Log Level Decision
|
||||||
```
|
```
|
||||||
Method flow details? → Trace
|
Method flow details? → Trace
|
||||||
Development debug? → Debug
|
Development debug? → Debug
|
||||||
Runtime information? → Info
|
Runtime information? → Info
|
||||||
Potential problem? → Warn
|
Potential problem? → Warn
|
||||||
Error occurred? → Error
|
Error occurred? → Error
|
||||||
```
|
```
|
||||||
|
|
||||||
## Performance Tips
|
## Performance Tips
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ DO: Lazy evaluation
|
// ✅ DO: Lazy evaluation
|
||||||
this.#logger.debug('Data', () => ({
|
this.#logger.debug('Data', () => ({
|
||||||
result: this.expensive() // Only runs if debug enabled
|
result: this.expensive() // Only runs if debug enabled
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ❌ DON'T: Eager evaluation
|
// ❌ DON'T: Eager evaluation
|
||||||
this.#logger.debug('Data', {
|
this.#logger.debug('Data', {
|
||||||
result: this.expensive() // Always runs
|
result: this.expensive() // Always runs
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ DO: Log aggregates
|
// ✅ DO: Log aggregates
|
||||||
this.#logger.info('Batch done', () => ({ count: items.length }));
|
this.#logger.info('Batch done', () => ({ count: items.length }));
|
||||||
|
|
||||||
// ❌ DON'T: Log in loops
|
// ❌ DON'T: Log in loops
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
this.#logger.debug('Item', { item }); // Performance hit
|
this.#logger.debug('Item', { item }); // Performance hit
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||||
import { LoggingService } from '@isa/core/logging';
|
import { LoggingService } from '@isa/core/logging';
|
||||||
|
|
||||||
describe('MyComponent', () => {
|
describe('MyComponent', () => {
|
||||||
const createComponent = createComponentFactory({
|
const createComponent = createComponentFactory({
|
||||||
component: MyComponent,
|
component: MyComponent,
|
||||||
mocks: [LoggingService]
|
mocks: [LoggingService]
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs error', () => {
|
it('logs error', () => {
|
||||||
const spectator = createComponent();
|
const spectator = createComponent();
|
||||||
const logger = spectator.inject(LoggingService);
|
const logger = spectator.inject(LoggingService);
|
||||||
|
|
||||||
spectator.component.operation();
|
spectator.component.operation();
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalled();
|
expect(logger.error).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom Sink
|
## Custom Sink
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Sink, LogLevel, LoggerContext } from '@isa/core/logging';
|
import { Sink, LogLevel, LoggerContext } from '@isa/core/logging';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomSink implements Sink {
|
export class CustomSink implements Sink {
|
||||||
log(level: LogLevel, message: string, context?: LoggerContext, error?: Error): void {
|
log(level: LogLevel, message: string, context?: LoggerContext, error?: Error): void {
|
||||||
// Implementation
|
// Implementation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
provideLogging(withSink(CustomSink))
|
provideLogging(withSink(CustomSink))
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sink Function (with DI)
|
## Sink Function (with DI)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { SinkFn, LogLevel } from '@isa/core/logging';
|
import { SinkFn, LogLevel } from '@isa/core/logging';
|
||||||
|
|
||||||
export const remoteSink: SinkFn = () => {
|
export const remoteSink: SinkFn = () => {
|
||||||
const http = inject(HttpClient);
|
const http = inject(HttpClient);
|
||||||
|
|
||||||
return (level, message, context, error) => {
|
return (level, message, context, error) => {
|
||||||
if (level === LogLevel.Error) {
|
if (level === LogLevel.Error) {
|
||||||
http.post('/api/logs', { level, message, context, error }).subscribe();
|
http.post('/api/logs', { level, message, context, error }).subscribe();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register
|
// Register
|
||||||
provideLogging(withSinkFn(remoteSink))
|
provideLogging(withSinkFn(remoteSink))
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Imports
|
## Common Imports
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Main imports
|
// Main imports
|
||||||
import { logger, provideLoggerContext } from '@isa/core/logging';
|
import { logger, provideLoggerContext } from '@isa/core/logging';
|
||||||
|
|
||||||
// Configuration imports
|
// Configuration imports
|
||||||
import {
|
import {
|
||||||
provideLogging,
|
provideLogging,
|
||||||
withLogLevel,
|
withLogLevel,
|
||||||
withSink,
|
withSink,
|
||||||
withContext,
|
withContext,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
ConsoleLogSink
|
ConsoleLogSink
|
||||||
} from '@isa/core/logging';
|
} from '@isa/core/logging';
|
||||||
|
|
||||||
// Type imports
|
// Type imports
|
||||||
import {
|
import {
|
||||||
LoggerApi,
|
LoggerApi,
|
||||||
Sink,
|
Sink,
|
||||||
SinkFn,
|
SinkFn,
|
||||||
LoggerContext
|
LoggerContext
|
||||||
} from '@isa/core/logging';
|
} from '@isa/core/logging';
|
||||||
```
|
```
|
||||||
@@ -1,350 +1,350 @@
|
|||||||
# Logging Examples
|
# Logging Examples
|
||||||
|
|
||||||
Concise real-world examples of logging patterns.
|
Concise real-world examples of logging patterns.
|
||||||
|
|
||||||
## 1. Component with Observable
|
## 1. Component with Observable
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-product-list',
|
selector: 'app-product-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class ProductListComponent implements OnInit {
|
export class ProductListComponent implements OnInit {
|
||||||
#logger = logger({ component: 'ProductListComponent' });
|
#logger = logger({ component: 'ProductListComponent' });
|
||||||
|
|
||||||
constructor(private productService: ProductService) {}
|
constructor(private productService: ProductService) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.#logger.info('Component initialized');
|
this.#logger.info('Component initialized');
|
||||||
this.loadProducts();
|
this.loadProducts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadProducts(): void {
|
private loadProducts(): void {
|
||||||
this.productService.getProducts().subscribe({
|
this.productService.getProducts().subscribe({
|
||||||
next: (products) => {
|
next: (products) => {
|
||||||
this.#logger.info('Products loaded', () => ({ count: products.length }));
|
this.#logger.info('Products loaded', () => ({ count: products.length }));
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.#logger.error('Failed to load products', error);
|
this.#logger.error('Failed to load products', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. Service with HTTP
|
## 2. Service with HTTP
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
import { catchError, tap } from 'rxjs/operators';
|
import { catchError, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class OrderService {
|
export class OrderService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
#logger = logger({ service: 'OrderService' });
|
#logger = logger({ service: 'OrderService' });
|
||||||
|
|
||||||
getOrder(id: string): Observable<Order> {
|
getOrder(id: string): Observable<Order> {
|
||||||
this.#logger.debug('Fetching order', { id });
|
this.#logger.debug('Fetching order', { id });
|
||||||
|
|
||||||
return this.http.get<Order>(`/api/orders/${id}`).pipe(
|
return this.http.get<Order>(`/api/orders/${id}`).pipe(
|
||||||
tap((order) => this.#logger.info('Order fetched', () => ({
|
tap((order) => this.#logger.info('Order fetched', () => ({
|
||||||
id,
|
id,
|
||||||
status: order.status
|
status: order.status
|
||||||
}))),
|
}))),
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
this.#logger.error('Fetch failed', error, () => ({ id, status: error.status }));
|
this.#logger.error('Fetch failed', error, () => ({ id, status: error.status }));
|
||||||
throw error;
|
throw error;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Hierarchical Context
|
## 3. Hierarchical Context
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { logger, provideLoggerContext } from '@isa/core/logging';
|
import { logger, provideLoggerContext } from '@isa/core/logging';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'oms-return-process',
|
selector: 'oms-return-process',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
providers: [
|
providers: [
|
||||||
provideLoggerContext({ feature: 'returns', module: 'oms' })
|
provideLoggerContext({ feature: 'returns', module: 'oms' })
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ReturnProcessComponent {
|
export class ReturnProcessComponent {
|
||||||
#logger = logger(() => ({
|
#logger = logger(() => ({
|
||||||
processId: this.currentProcessId,
|
processId: this.currentProcessId,
|
||||||
step: this.currentStep
|
step: this.currentStep
|
||||||
}));
|
}));
|
||||||
|
|
||||||
private currentProcessId = crypto.randomUUID();
|
private currentProcessId = crypto.randomUUID();
|
||||||
private currentStep = 1;
|
private currentStep = 1;
|
||||||
|
|
||||||
startProcess(orderId: string): void {
|
startProcess(orderId: string): void {
|
||||||
// Logs include: feature, module, processId, step, orderId
|
// Logs include: feature, module, processId, step, orderId
|
||||||
this.#logger.info('Process started', { orderId });
|
this.#logger.info('Process started', { orderId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. NgRx Effect
|
## 4. NgRx Effect
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
import { map, catchError, tap } from 'rxjs/operators';
|
import { map, catchError, tap } from 'rxjs/operators';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrdersEffects {
|
export class OrdersEffects {
|
||||||
#logger = logger({ effect: 'OrdersEffects' });
|
#logger = logger({ effect: 'OrdersEffects' });
|
||||||
|
|
||||||
loadOrders$ = createEffect(() =>
|
loadOrders$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(OrdersActions.loadOrders),
|
ofType(OrdersActions.loadOrders),
|
||||||
tap((action) => this.#logger.debug('Loading orders', () => ({
|
tap((action) => this.#logger.debug('Loading orders', () => ({
|
||||||
page: action.page
|
page: action.page
|
||||||
}))),
|
}))),
|
||||||
mergeMap((action) =>
|
mergeMap((action) =>
|
||||||
this.orderService.getOrders(action.filters).pipe(
|
this.orderService.getOrders(action.filters).pipe(
|
||||||
map((orders) => {
|
map((orders) => {
|
||||||
this.#logger.info('Orders loaded', () => ({ count: orders.length }));
|
this.#logger.info('Orders loaded', () => ({ count: orders.length }));
|
||||||
return OrdersActions.loadOrdersSuccess({ orders });
|
return OrdersActions.loadOrdersSuccess({ orders });
|
||||||
}),
|
}),
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
this.#logger.error('Load failed', error);
|
this.#logger.error('Load failed', error);
|
||||||
return of(OrdersActions.loadOrdersFailure({ error }));
|
return of(OrdersActions.loadOrdersFailure({ error }));
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private actions$: Actions,
|
private actions$: Actions,
|
||||||
private orderService: OrderService
|
private orderService: OrderService
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Guard with Authorization
|
## 5. Guard with Authorization
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { CanActivateFn, Router } from '@angular/router';
|
import { CanActivateFn, Router } from '@angular/router';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
|
|
||||||
export const authGuard: CanActivateFn = (route, state) => {
|
export const authGuard: CanActivateFn = (route, state) => {
|
||||||
const authService = inject(AuthService);
|
const authService = inject(AuthService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
const log = logger({ guard: 'AuthGuard' });
|
const log = logger({ guard: 'AuthGuard' });
|
||||||
|
|
||||||
if (authService.isAuthenticated()) {
|
if (authService.isAuthenticated()) {
|
||||||
log.debug('Access granted', () => ({ route: state.url }));
|
log.debug('Access granted', () => ({ route: state.url }));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn('Access denied', () => ({
|
log.warn('Access denied', () => ({
|
||||||
attemptedRoute: state.url,
|
attemptedRoute: state.url,
|
||||||
redirectTo: '/login'
|
redirectTo: '/login'
|
||||||
}));
|
}));
|
||||||
return router.createUrlTree(['/login']);
|
return router.createUrlTree(['/login']);
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## 6. HTTP Interceptor
|
## 6. HTTP Interceptor
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { HttpInterceptorFn } from '@angular/common/http';
|
import { HttpInterceptorFn } from '@angular/common/http';
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { tap, catchError } from 'rxjs/operators';
|
import { tap, catchError } from 'rxjs/operators';
|
||||||
import { LoggingService } from '@isa/core/logging';
|
import { LoggingService } from '@isa/core/logging';
|
||||||
|
|
||||||
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
|
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
const loggingService = inject(LoggingService);
|
const loggingService = inject(LoggingService);
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
loggingService.debug('HTTP Request', () => ({
|
loggingService.debug('HTTP Request', () => ({
|
||||||
method: req.method,
|
method: req.method,
|
||||||
url: req.url
|
url: req.url
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return next(req).pipe(
|
return next(req).pipe(
|
||||||
tap((event) => {
|
tap((event) => {
|
||||||
if (event.type === HttpEventType.Response) {
|
if (event.type === HttpEventType.Response) {
|
||||||
loggingService.info('HTTP Response', () => ({
|
loggingService.info('HTTP Response', () => ({
|
||||||
method: req.method,
|
method: req.method,
|
||||||
url: req.url,
|
url: req.url,
|
||||||
status: event.status,
|
status: event.status,
|
||||||
duration: `${(performance.now() - startTime).toFixed(2)}ms`
|
duration: `${(performance.now() - startTime).toFixed(2)}ms`
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
loggingService.error('HTTP Error', error, () => ({
|
loggingService.error('HTTP Error', error, () => ({
|
||||||
method: req.method,
|
method: req.method,
|
||||||
url: req.url,
|
url: req.url,
|
||||||
status: error.status
|
status: error.status
|
||||||
}));
|
}));
|
||||||
return throwError(() => error);
|
return throwError(() => error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7. Form Validation
|
## 7. Form Validation
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'shared-user-form',
|
selector: 'shared-user-form',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class UserFormComponent implements OnInit {
|
export class UserFormComponent implements OnInit {
|
||||||
#logger = logger({ component: 'UserFormComponent' });
|
#logger = logger({ component: 'UserFormComponent' });
|
||||||
form!: FormGroup;
|
form!: FormGroup;
|
||||||
|
|
||||||
constructor(private fb: FormBuilder) {}
|
constructor(private fb: FormBuilder) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.form = this.fb.group({
|
this.form = this.fb.group({
|
||||||
name: ['', Validators.required],
|
name: ['', Validators.required],
|
||||||
email: ['', [Validators.required, Validators.email]]
|
email: ['', [Validators.required, Validators.email]]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
if (this.form.invalid) {
|
if (this.form.invalid) {
|
||||||
this.#logger.warn('Invalid form submission', () => ({
|
this.#logger.warn('Invalid form submission', () => ({
|
||||||
errors: this.getFormErrors()
|
errors: this.getFormErrors()
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#logger.info('Form submitted');
|
this.#logger.info('Form submitted');
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFormErrors(): Record<string, unknown> {
|
private getFormErrors(): Record<string, unknown> {
|
||||||
const errors: Record<string, unknown> = {};
|
const errors: Record<string, unknown> = {};
|
||||||
Object.keys(this.form.controls).forEach((key) => {
|
Object.keys(this.form.controls).forEach((key) => {
|
||||||
const control = this.form.get(key);
|
const control = this.form.get(key);
|
||||||
if (control?.errors) errors[key] = control.errors;
|
if (control?.errors) errors[key] = control.errors;
|
||||||
});
|
});
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 8. Async Progress Tracking
|
## 8. Async Progress Tracking
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ImportService {
|
export class ImportService {
|
||||||
#logger = logger({ service: 'ImportService' });
|
#logger = logger({ service: 'ImportService' });
|
||||||
|
|
||||||
importData(file: File): Observable<number> {
|
importData(file: File): Observable<number> {
|
||||||
const importId = crypto.randomUUID();
|
const importId = crypto.randomUUID();
|
||||||
|
|
||||||
this.#logger.info('Import started', () => ({
|
this.#logger.info('Import started', () => ({
|
||||||
importId,
|
importId,
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
fileSize: file.size
|
fileSize: file.size
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return this.processImport(file).pipe(
|
return this.processImport(file).pipe(
|
||||||
tap((progress) => {
|
tap((progress) => {
|
||||||
if (progress % 25 === 0) {
|
if (progress % 25 === 0) {
|
||||||
this.#logger.debug('Import progress', () => ({
|
this.#logger.debug('Import progress', () => ({
|
||||||
importId,
|
importId,
|
||||||
progress: `${progress}%`
|
progress: `${progress}%`
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
tap({
|
tap({
|
||||||
complete: () => this.#logger.info('Import completed', { importId }),
|
complete: () => this.#logger.info('Import completed', { importId }),
|
||||||
error: (error) => this.#logger.error('Import failed', error, { importId })
|
error: (error) => this.#logger.error('Import failed', error, { importId })
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private processImport(file: File): Observable<number> {
|
private processImport(file: File): Observable<number> {
|
||||||
// Implementation
|
// Implementation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 9. Global Error Handler
|
## 9. Global Error Handler
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Injectable, ErrorHandler } from '@angular/core';
|
import { Injectable, ErrorHandler } from '@angular/core';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GlobalErrorHandler implements ErrorHandler {
|
export class GlobalErrorHandler implements ErrorHandler {
|
||||||
#logger = logger({ handler: 'GlobalErrorHandler' });
|
#logger = logger({ handler: 'GlobalErrorHandler' });
|
||||||
|
|
||||||
handleError(error: Error): void {
|
handleError(error: Error): void {
|
||||||
this.#logger.error('Uncaught error', error, () => ({
|
this.#logger.error('Uncaught error', error, () => ({
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
userAgent: navigator.userAgent,
|
userAgent: navigator.userAgent,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 10. WebSocket Component
|
## 10. WebSocket Component
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { logger } from '@isa/core/logging';
|
import { logger } from '@isa/core/logging';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'oms-live-orders',
|
selector: 'oms-live-orders',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class LiveOrdersComponent implements OnInit, OnDestroy {
|
export class LiveOrdersComponent implements OnInit, OnDestroy {
|
||||||
#logger = logger({ component: 'LiveOrdersComponent' });
|
#logger = logger({ component: 'LiveOrdersComponent' });
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
constructor(private wsService: WebSocketService) {}
|
constructor(private wsService: WebSocketService) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.#logger.info('Connecting to WebSocket');
|
this.#logger.info('Connecting to WebSocket');
|
||||||
|
|
||||||
this.wsService.connect('orders').pipe(
|
this.wsService.connect('orders').pipe(
|
||||||
takeUntil(this.destroy$)
|
takeUntil(this.destroy$)
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: (msg) => this.#logger.debug('Message received', () => ({
|
next: (msg) => this.#logger.debug('Message received', () => ({
|
||||||
type: msg.type,
|
type: msg.type,
|
||||||
orderId: msg.orderId
|
orderId: msg.orderId
|
||||||
})),
|
})),
|
||||||
error: (error) => this.#logger.error('WebSocket error', error),
|
error: (error) => this.#logger.error('WebSocket error', error),
|
||||||
complete: () => this.#logger.info('WebSocket closed')
|
complete: () => this.#logger.info('WebSocket closed')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.#logger.debug('Component destroyed');
|
this.#logger.debug('Component destroyed');
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -1,235 +1,235 @@
|
|||||||
# Logging Troubleshooting
|
# Logging Troubleshooting
|
||||||
|
|
||||||
## 1. Logs Not Appearing
|
## 1. Logs Not Appearing
|
||||||
|
|
||||||
**Problem:** Logger called but nothing in console.
|
**Problem:** Logger called but nothing in console.
|
||||||
|
|
||||||
**Solutions:**
|
**Solutions:**
|
||||||
```typescript
|
```typescript
|
||||||
// Check log level
|
// Check log level
|
||||||
provideLogging(
|
provideLogging(
|
||||||
withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn)
|
withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add sink
|
// Add sink
|
||||||
provideLogging(
|
provideLogging(
|
||||||
withLogLevel(LogLevel.Debug),
|
withLogLevel(LogLevel.Debug),
|
||||||
withSink(ConsoleLogSink) // Required!
|
withSink(ConsoleLogSink) // Required!
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verify configuration in app.config.ts
|
// Verify configuration in app.config.ts
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideLogging(...) // Must be present
|
provideLogging(...) // Must be present
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. NullInjectorError
|
## 2. NullInjectorError
|
||||||
|
|
||||||
**Error:** `NullInjectorError: No provider for LoggingService!`
|
**Error:** `NullInjectorError: No provider for LoggingService!`
|
||||||
|
|
||||||
**Solution:**
|
**Solution:**
|
||||||
```typescript
|
```typescript
|
||||||
// app.config.ts
|
// app.config.ts
|
||||||
import { provideLogging, withLogLevel, withSink,
|
import { provideLogging, withLogLevel, withSink,
|
||||||
LogLevel, ConsoleLogSink } from '@isa/core/logging';
|
LogLevel, ConsoleLogSink } from '@isa/core/logging';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideLogging(
|
provideLogging(
|
||||||
withLogLevel(LogLevel.Debug),
|
withLogLevel(LogLevel.Debug),
|
||||||
withSink(ConsoleLogSink)
|
withSink(ConsoleLogSink)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Context Not Showing
|
## 3. Context Not Showing
|
||||||
|
|
||||||
**Problem:** Context passed but doesn't appear.
|
**Problem:** Context passed but doesn't appear.
|
||||||
|
|
||||||
**Check:**
|
**Check:**
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ Both work:
|
// ✅ Both work:
|
||||||
this.#logger.info('Message', () => ({ id: '123' })); // Function
|
this.#logger.info('Message', () => ({ id: '123' })); // Function
|
||||||
this.#logger.info('Message', { id: '123' }); // Object
|
this.#logger.info('Message', { id: '123' }); // Object
|
||||||
|
|
||||||
// ❌ Common mistake:
|
// ❌ Common mistake:
|
||||||
const ctx = { id: '123' };
|
const ctx = { id: '123' };
|
||||||
this.#logger.info('Message', ctx); // Actually works!
|
this.#logger.info('Message', ctx); // Actually works!
|
||||||
|
|
||||||
// Verify hierarchical merge:
|
// Verify hierarchical merge:
|
||||||
// Global → Component → Instance → Message
|
// Global → Component → Instance → Message
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Performance Issues
|
## 4. Performance Issues
|
||||||
|
|
||||||
**Problem:** Slow when debug logging enabled.
|
**Problem:** Slow when debug logging enabled.
|
||||||
|
|
||||||
**Solutions:**
|
**Solutions:**
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ Use lazy evaluation
|
// ✅ Use lazy evaluation
|
||||||
this.#logger.debug('Data', () => ({
|
this.#logger.debug('Data', () => ({
|
||||||
expensive: this.compute() // Only if debug enabled
|
expensive: this.compute() // Only if debug enabled
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ✅ Reduce log frequency
|
// ✅ Reduce log frequency
|
||||||
this.#logger.debug('Batch', () => ({
|
this.#logger.debug('Batch', () => ({
|
||||||
count: items.length // Not each item
|
count: items.length // Not each item
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ✅ Increase production level
|
// ✅ Increase production level
|
||||||
provideLogging(
|
provideLogging(
|
||||||
withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn)
|
withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn)
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Error Object Not Logged
|
## 5. Error Object Not Logged
|
||||||
|
|
||||||
**Problem:** Error shows as `[object Object]`.
|
**Problem:** Error shows as `[object Object]`.
|
||||||
|
|
||||||
**Solution:**
|
**Solution:**
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ Wrong
|
// ❌ Wrong
|
||||||
this.#logger.error('Failed', { error }); // Don't wrap in object
|
this.#logger.error('Failed', { error }); // Don't wrap in object
|
||||||
|
|
||||||
// ✅ Correct
|
// ✅ Correct
|
||||||
this.#logger.error('Failed', error as Error, () => ({
|
this.#logger.error('Failed', error as Error, () => ({
|
||||||
additionalContext: 'value'
|
additionalContext: 'value'
|
||||||
}));
|
}));
|
||||||
```
|
```
|
||||||
|
|
||||||
## 6. TypeScript Errors
|
## 6. TypeScript Errors
|
||||||
|
|
||||||
**Error:** `Type 'X' is not assignable to 'MaybeLoggerContextFn'`
|
**Error:** `Type 'X' is not assignable to 'MaybeLoggerContextFn'`
|
||||||
|
|
||||||
**Solution:**
|
**Solution:**
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ Wrong type
|
// ❌ Wrong type
|
||||||
this.#logger.info('Message', 'string'); // Invalid
|
this.#logger.info('Message', 'string'); // Invalid
|
||||||
|
|
||||||
// ✅ Correct types
|
// ✅ Correct types
|
||||||
this.#logger.info('Message', { key: 'value' });
|
this.#logger.info('Message', { key: 'value' });
|
||||||
this.#logger.info('Message', () => ({ key: 'value' }));
|
this.#logger.info('Message', () => ({ key: 'value' }));
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7. Logs in Tests
|
## 7. Logs in Tests
|
||||||
|
|
||||||
**Problem:** Test output cluttered with logs.
|
**Problem:** Test output cluttered with logs.
|
||||||
|
|
||||||
**Solutions:**
|
**Solutions:**
|
||||||
```typescript
|
```typescript
|
||||||
// Mock logging service
|
// Mock logging service
|
||||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||||
import { LoggingService } from '@isa/core/logging';
|
import { LoggingService } from '@isa/core/logging';
|
||||||
|
|
||||||
const createComponent = createComponentFactory({
|
const createComponent = createComponentFactory({
|
||||||
component: MyComponent,
|
component: MyComponent,
|
||||||
mocks: [LoggingService] // Mocks all log methods
|
mocks: [LoggingService] // Mocks all log methods
|
||||||
});
|
});
|
||||||
|
|
||||||
// Or disable in tests
|
// Or disable in tests
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
provideLogging(withLogLevel(LogLevel.Off))
|
provideLogging(withLogLevel(LogLevel.Off))
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## 8. Undefined Property Error
|
## 8. Undefined Property Error
|
||||||
|
|
||||||
**Error:** `Cannot read property 'X' of undefined`
|
**Error:** `Cannot read property 'X' of undefined`
|
||||||
|
|
||||||
**Problem:** Accessing uninitialized property in logger context.
|
**Problem:** Accessing uninitialized property in logger context.
|
||||||
|
|
||||||
**Solutions:**
|
**Solutions:**
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ Problem
|
// ❌ Problem
|
||||||
#logger = logger(() => ({
|
#logger = logger(() => ({
|
||||||
userId: this.userService.currentUserId // May be undefined
|
userId: this.userService.currentUserId // May be undefined
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ✅ Solution 1: Optional chaining
|
// ✅ Solution 1: Optional chaining
|
||||||
#logger = logger(() => ({
|
#logger = logger(() => ({
|
||||||
userId: this.userService?.currentUserId ?? 'unknown'
|
userId: this.userService?.currentUserId ?? 'unknown'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ✅ Solution 2: Delay access
|
// ✅ Solution 2: Delay access
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.#logger.info('Init', () => ({
|
this.#logger.info('Init', () => ({
|
||||||
userId: this.userService.currentUserId // Safe here
|
userId: this.userService.currentUserId // Safe here
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 9. Circular Dependency
|
## 9. Circular Dependency
|
||||||
|
|
||||||
**Error:** `NG0200: Circular dependency in DI detected`
|
**Error:** `NG0200: Circular dependency in DI detected`
|
||||||
|
|
||||||
**Cause:** Service A ← → Service B both inject LoggingService.
|
**Cause:** Service A ← → Service B both inject LoggingService.
|
||||||
|
|
||||||
**Solution:**
|
**Solution:**
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ Creates circular dependency
|
// ❌ Creates circular dependency
|
||||||
constructor(private loggingService: LoggingService) {}
|
constructor(private loggingService: LoggingService) {}
|
||||||
|
|
||||||
// ✅ Use factory (no circular dependency)
|
// ✅ Use factory (no circular dependency)
|
||||||
#logger = logger({ service: 'MyService' });
|
#logger = logger({ service: 'MyService' });
|
||||||
```
|
```
|
||||||
|
|
||||||
## 10. Custom Sink Not Working
|
## 10. Custom Sink Not Working
|
||||||
|
|
||||||
**Problem:** Sink registered but never called.
|
**Problem:** Sink registered but never called.
|
||||||
|
|
||||||
**Solutions:**
|
**Solutions:**
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ Correct registration
|
// ✅ Correct registration
|
||||||
provideLogging(
|
provideLogging(
|
||||||
withSink(MySink) // Add to config
|
withSink(MySink) // Add to config
|
||||||
)
|
)
|
||||||
|
|
||||||
// ✅ Correct signature
|
// ✅ Correct signature
|
||||||
export class MySink implements Sink {
|
export class MySink implements Sink {
|
||||||
log(
|
log(
|
||||||
level: LogLevel,
|
level: LogLevel,
|
||||||
message: string,
|
message: string,
|
||||||
context?: LoggerContext,
|
context?: LoggerContext,
|
||||||
error?: Error
|
error?: Error
|
||||||
): void {
|
): void {
|
||||||
// Implementation
|
// Implementation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Sink function must return function
|
// ✅ Sink function must return function
|
||||||
export const mySinkFn: SinkFn = () => {
|
export const mySinkFn: SinkFn = () => {
|
||||||
const http = inject(HttpClient);
|
const http = inject(HttpClient);
|
||||||
return (level, message, context, error) => {
|
return (level, message, context, error) => {
|
||||||
// Implementation
|
// Implementation
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Diagnostics
|
## Quick Diagnostics
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Enable all logs temporarily
|
// Enable all logs temporarily
|
||||||
provideLogging(withLogLevel(LogLevel.Trace))
|
provideLogging(withLogLevel(LogLevel.Trace))
|
||||||
|
|
||||||
// Check imports
|
// Check imports
|
||||||
import { logger } from '@isa/core/logging'; // ✅ Correct
|
import { logger } from '@isa/core/logging'; // ✅ Correct
|
||||||
import { logger } from '@isa/core/logging/src/lib/logger.factory'; // ❌ Wrong
|
import { logger } from '@isa/core/logging/src/lib/logger.factory'; // ❌ Wrong
|
||||||
|
|
||||||
// Verify console filters in browser DevTools
|
// Verify console filters in browser DevTools
|
||||||
// Ensure Info, Debug, Warnings are enabled
|
// Ensure Info, Debug, Warnings are enabled
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Error Messages
|
## Common Error Messages
|
||||||
|
|
||||||
| Error | Cause | Fix |
|
| Error | Cause | Fix |
|
||||||
|-------|-------|-----|
|
|-------|-------|-----|
|
||||||
| `NullInjectorError: LoggingService` | Missing config | Add `provideLogging()` |
|
| `NullInjectorError: LoggingService` | Missing config | Add `provideLogging()` |
|
||||||
| `Type 'X' not assignable` | Wrong context type | Use object or function |
|
| `Type 'X' not assignable` | Wrong context type | Use object or function |
|
||||||
| `Cannot read property 'X'` | Undefined property | Use optional chaining |
|
| `Cannot read property 'X'` | Undefined property | Use optional chaining |
|
||||||
| `Circular dependency` | Service injection | Use `logger()` factory |
|
| `Circular dependency` | Service injection | Use `logger()` factory |
|
||||||
| Stack overflow | Infinite loop in context | Don't call logger in context |
|
| Stack overflow | Infinite loop in context | Don't call logger in context |
|
||||||
@@ -9,29 +9,6 @@ description: This skill should be used when working with Tailwind CSS styling in
|
|||||||
|
|
||||||
Assist with applying the ISA-specific Tailwind CSS design system throughout the ISA-Frontend Angular monorepo. This skill provides comprehensive knowledge of custom utilities, color palettes, typography classes, button variants, and layout patterns specific to this project.
|
Assist with applying the ISA-specific Tailwind CSS design system throughout the ISA-Frontend Angular monorepo. This skill provides comprehensive knowledge of custom utilities, color palettes, typography classes, button variants, and layout patterns specific to this project.
|
||||||
|
|
||||||
## When to Use This Skill
|
|
||||||
|
|
||||||
Invoke this skill when:
|
|
||||||
- **After** checking `libs/ui/**` for existing components (always check first!)
|
|
||||||
- Styling layout and spacing for components
|
|
||||||
- Choosing appropriate color values for custom elements
|
|
||||||
- Applying typography classes to text content
|
|
||||||
- Determining spacing, layout, or responsive breakpoints
|
|
||||||
- Customizing or extending existing UI components
|
|
||||||
- Ensuring design system consistency
|
|
||||||
- Questions about which Tailwind utility classes are available
|
|
||||||
|
|
||||||
**Important**: This skill provides Tailwind utilities. Always prefer using components from `@isa/ui/*` libraries before applying custom Tailwind styles.
|
|
||||||
|
|
||||||
**Works together with:**
|
|
||||||
- **[template-standards](../template-standards/SKILL.md)** - Angular template syntax, E2E testing attributes, and ARIA accessibility
|
|
||||||
- **[logging](../logging/SKILL.md)** - MANDATORY logging in all Angular files
|
|
||||||
|
|
||||||
When building Angular components, these skills work together:
|
|
||||||
1. Use **template-standards** for Angular syntax, `data-*`, and ARIA attributes
|
|
||||||
2. Use **tailwind** (this skill) for styling with the ISA design system
|
|
||||||
3. Use **logging** for all component/service logging
|
|
||||||
|
|
||||||
## Core Design System Principles
|
## Core Design System Principles
|
||||||
|
|
||||||
### 0. Component Libraries First (Most Important)
|
### 0. Component Libraries First (Most Important)
|
||||||
|
|||||||
@@ -7,20 +7,6 @@ description: This skill should be used when writing or reviewing Angular compone
|
|||||||
|
|
||||||
Comprehensive guide for Angular templates covering modern syntax, E2E testing attributes, and ARIA accessibility.
|
Comprehensive guide for Angular templates covering modern syntax, E2E testing attributes, and ARIA accessibility.
|
||||||
|
|
||||||
## When to Use
|
|
||||||
|
|
||||||
- Creating or reviewing component templates
|
|
||||||
- Refactoring legacy `*ngIf/*ngFor/*ngSwitch` to modern syntax
|
|
||||||
- Implementing `@defer` lazy loading
|
|
||||||
- Designing reusable components with `ng-content`
|
|
||||||
- Adding E2E testing attributes for automated tests
|
|
||||||
- Ensuring WCAG accessibility compliance
|
|
||||||
- Template performance optimization
|
|
||||||
|
|
||||||
**Related Skills:**
|
|
||||||
- **[tailwind](../tailwind/SKILL.md)** - ISA design system styling (colors, typography, spacing, layout)
|
|
||||||
- **[logging](../logging/SKILL.md)** - MANDATORY logging in all Angular files using `@isa/core/logging`
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This skill combines three essential aspects of Angular template development:
|
This skill combines three essential aspects of Angular template development:
|
||||||
@@ -31,7 +17,9 @@ This skill combines three essential aspects of Angular template development:
|
|||||||
|
|
||||||
**Every interactive element MUST include both E2E and ARIA attributes.**
|
**Every interactive element MUST include both E2E and ARIA attributes.**
|
||||||
|
|
||||||
---
|
**Related Skills:**
|
||||||
|
- **tailwind** - ISA design system styling (colors, typography, spacing, layout)
|
||||||
|
- **logging** - MANDATORY logging in all Angular files using `@isa/core/logging`
|
||||||
|
|
||||||
## Part 1: Angular Template Syntax
|
## Part 1: Angular Template Syntax
|
||||||
|
|
||||||
@@ -81,6 +69,8 @@ This skill combines three essential aspects of Angular template development:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**See [control-flow-reference.md](references/control-flow-reference.md) for advanced patterns including nested loops, complex conditions, and filter strategies.**
|
||||||
|
|
||||||
### @defer Lazy Loading
|
### @defer Lazy Loading
|
||||||
|
|
||||||
#### Basic Usage
|
#### Basic Usage
|
||||||
@@ -97,7 +87,7 @@ This skill combines three essential aspects of Angular template development:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Triggers
|
#### Common Triggers
|
||||||
|
|
||||||
| Trigger | Use Case |
|
| Trigger | Use Case |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
@@ -111,18 +101,15 @@ This skill combines three essential aspects of Angular template development:
|
|||||||
**Multiple triggers:** `@defer (on interaction; on timer(5s))`
|
**Multiple triggers:** `@defer (on interaction; on timer(5s))`
|
||||||
**Prefetching:** `@defer (on interaction; prefetch on idle)`
|
**Prefetching:** `@defer (on interaction; prefetch on idle)`
|
||||||
|
|
||||||
#### Requirements
|
#### Critical Requirements
|
||||||
|
|
||||||
- Components **MUST be standalone**
|
- Components **MUST be standalone**
|
||||||
- No `@ViewChild`/`@ContentChild` references
|
- No `@ViewChild`/`@ContentChild` references
|
||||||
- Reserve space in `@placeholder` to prevent layout shift
|
- Reserve space in `@placeholder` to prevent layout shift
|
||||||
|
- Never defer above-the-fold content (harms LCP)
|
||||||
|
- Avoid `immediate`/`timer` during initial render (harms TTI)
|
||||||
|
|
||||||
#### Best Practices
|
**See [defer-patterns.md](references/defer-patterns.md) for performance optimization, Core Web Vitals impact, bundle size reduction strategies, and real-world examples.**
|
||||||
|
|
||||||
- ✅ Defer below-the-fold content
|
|
||||||
- ❌ Never defer above-the-fold (harms LCP)
|
|
||||||
- ❌ Avoid `immediate`/`timer` during initial render (harms TTI)
|
|
||||||
- Test with network throttling
|
|
||||||
|
|
||||||
### Content Projection
|
### Content Projection
|
||||||
|
|
||||||
@@ -156,12 +143,9 @@ This skill combines three essential aspects of Angular template development:
|
|||||||
</ui-card>
|
</ui-card>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Fallback content:** `<ng-content select="title">Default Title</ng-content>`
|
**CRITICAL Constraint:** `ng-content` **always instantiates** (even if hidden). For conditional projection, use `ng-template` + `NgTemplateOutlet`.
|
||||||
**Aliasing:** `<h3 ngProjectAs="card-header">Title</h3>`
|
|
||||||
|
|
||||||
#### CRITICAL Constraint
|
**See [projection-patterns.md](references/projection-patterns.md) for conditional projection, template-based projection, querying projected content, and modal/form field examples.**
|
||||||
|
|
||||||
`ng-content` **always instantiates** (even if hidden). For conditional projection, use `ng-template` + `NgTemplateOutlet`.
|
|
||||||
|
|
||||||
### Template References
|
### Template References
|
||||||
|
|
||||||
@@ -193,6 +177,8 @@ Groups elements without DOM footprint:
|
|||||||
</p>
|
</p>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**See [template-reference.md](references/template-reference.md) for programmatic rendering, ViewContainerRef patterns, context scoping, and common pitfalls.**
|
||||||
|
|
||||||
### Variables
|
### Variables
|
||||||
|
|
||||||
#### @let (Angular 18.1+)
|
#### @let (Angular 18.1+)
|
||||||
@@ -226,16 +212,14 @@ Groups elements without DOM footprint:
|
|||||||
**Class:** `[class.active]="isActive()"` or `[class]="{active: isActive()}"`
|
**Class:** `[class.active]="isActive()"` or `[class]="{active: isActive()}"`
|
||||||
**Style:** `[style.width.px]="width()"` or `[style]="{color: textColor()}"`
|
**Style:** `[style.width.px]="width()"` or `[style]="{color: textColor()}"`
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 2: E2E Testing Attributes
|
## Part 2: E2E Testing Attributes
|
||||||
|
|
||||||
### Purpose
|
### Purpose
|
||||||
|
|
||||||
Enable automated end-to-end testing by providing stable selectors for QA automation:
|
Enable automated end-to-end testing by providing stable selectors for QA automation:
|
||||||
- **`data-what`**: Semantic description of element's purpose
|
- **`data-what`**: Semantic description of element's purpose (e.g., `submit-button`, `email-input`)
|
||||||
- **`data-which`**: Unique identifier for specific instances
|
- **`data-which`**: Unique identifier for specific instances (e.g., `registration-form`, `customer-123`)
|
||||||
- **`data-*`**: Additional contextual information
|
- **`data-*`**: Additional contextual information (e.g., `data-status="active"`)
|
||||||
|
|
||||||
### Naming Conventions
|
### Naming Conventions
|
||||||
|
|
||||||
@@ -259,9 +243,8 @@ Enable automated end-to-end testing by providing stable selectors for QA automat
|
|||||||
3. ✅ Ensure `data-which` is unique within the view
|
3. ✅ Ensure `data-which` is unique within the view
|
||||||
4. ✅ Use Angular binding for dynamic values: `[attr.data-*]`
|
4. ✅ Use Angular binding for dynamic values: `[attr.data-*]`
|
||||||
5. ✅ Avoid including sensitive data in attributes
|
5. ✅ Avoid including sensitive data in attributes
|
||||||
6. ✅ Document complex attribute patterns in template comments
|
|
||||||
|
|
||||||
---
|
**See [e2e-attributes.md](references/e2e-attributes.md) for complete patterns by element type (buttons, inputs, links, lists, tables, dialogs), dynamic attribute bindings, testing integration examples, and validation strategies.**
|
||||||
|
|
||||||
## Part 3: ARIA Accessibility Attributes
|
## Part 3: ARIA Accessibility Attributes
|
||||||
|
|
||||||
@@ -290,7 +273,7 @@ Ensure web applications are accessible to all users, including those using assis
|
|||||||
6. ✅ Keep ARIA attributes in sync with visual states
|
6. ✅ Keep ARIA attributes in sync with visual states
|
||||||
7. ✅ Test with screen readers (NVDA, JAWS, VoiceOver)
|
7. ✅ Test with screen readers (NVDA, JAWS, VoiceOver)
|
||||||
|
|
||||||
---
|
**See [aria-attributes.md](references/aria-attributes.md) for comprehensive role reference, property and state attributes, live regions, keyboard navigation patterns, WCAG compliance requirements, and testing strategies.**
|
||||||
|
|
||||||
## Part 4: Combined Examples
|
## Part 4: Combined Examples
|
||||||
|
|
||||||
@@ -342,18 +325,6 @@ Ensure web applications are accessible to all users, including those using assis
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Link with All Attributes
|
|
||||||
|
|
||||||
```html
|
|
||||||
<a
|
|
||||||
[routerLink]="['/orders', orderId]"
|
|
||||||
data-what="order-link"
|
|
||||||
[attr.data-which]="orderId"
|
|
||||||
[attr.aria-label]="'View order ' + orderNumber">
|
|
||||||
View Order #{{ orderNumber }}
|
|
||||||
</a>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dialog with All Attributes
|
### Dialog with All Attributes
|
||||||
|
|
||||||
```html
|
```html
|
||||||
@@ -387,109 +358,7 @@ Ensure web applications are accessible to all users, including those using assis
|
|||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Complete Form Example
|
**See [combined-patterns.md](references/combined-patterns.md) for complete form examples, product listings, shopping carts, modal dialogs, navigation patterns, data tables, search interfaces, notifications, and multi-step forms with all attributes properly applied.**
|
||||||
|
|
||||||
```html
|
|
||||||
<form
|
|
||||||
(ngSubmit)="onSubmit()"
|
|
||||||
data-what="registration-form"
|
|
||||||
data-which="user-signup"
|
|
||||||
role="form"
|
|
||||||
aria-labelledby="form-title">
|
|
||||||
|
|
||||||
<h2 id="form-title">User Registration</h2>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="username-input">Username</label>
|
|
||||||
<input
|
|
||||||
id="username-input"
|
|
||||||
type="text"
|
|
||||||
[(ngModel)]="username"
|
|
||||||
name="username"
|
|
||||||
data-what="username-input"
|
|
||||||
data-which="registration-form"
|
|
||||||
aria-required="true"
|
|
||||||
aria-describedby="username-hint" />
|
|
||||||
<span id="username-hint">Must be at least 3 characters</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="email-input">Email</label>
|
|
||||||
<input
|
|
||||||
id="email-input"
|
|
||||||
type="email"
|
|
||||||
[(ngModel)]="email"
|
|
||||||
name="email"
|
|
||||||
data-what="email-input"
|
|
||||||
data-which="registration-form"
|
|
||||||
aria-required="true"
|
|
||||||
[attr.aria-invalid]="emailError ? 'true' : null"
|
|
||||||
aria-describedby="email-error" />
|
|
||||||
@if (emailError) {
|
|
||||||
<span
|
|
||||||
id="email-error"
|
|
||||||
role="alert"
|
|
||||||
aria-live="polite">
|
|
||||||
{{ emailError }}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
data-what="submit-button"
|
|
||||||
data-which="registration-form"
|
|
||||||
[attr.aria-disabled]="!isValid()"
|
|
||||||
aria-label="Submit registration form">
|
|
||||||
Register
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="onCancel()"
|
|
||||||
data-what="cancel-button"
|
|
||||||
data-which="registration-form"
|
|
||||||
aria-label="Cancel registration">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conditional Rendering with Attributes
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@if (isLoading()) {
|
|
||||||
<div
|
|
||||||
data-what="loading-spinner"
|
|
||||||
data-which="user-profile"
|
|
||||||
role="status"
|
|
||||||
aria-live="polite"
|
|
||||||
aria-label="Loading user profile">
|
|
||||||
<mat-spinner />
|
|
||||||
</div>
|
|
||||||
} @else if (error()) {
|
|
||||||
<div
|
|
||||||
data-what="error-message"
|
|
||||||
data-which="user-profile"
|
|
||||||
role="alert"
|
|
||||||
aria-live="assertive">
|
|
||||||
{{ error() }}
|
|
||||||
</div>
|
|
||||||
} @else {
|
|
||||||
<div
|
|
||||||
data-what="profile-content"
|
|
||||||
data-which="user-profile"
|
|
||||||
role="region"
|
|
||||||
aria-labelledby="profile-title">
|
|
||||||
<h2 id="profile-title">{{ user().name }}</h2>
|
|
||||||
<!-- profile content -->
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Validation Checklist
|
## Validation Checklist
|
||||||
|
|
||||||
@@ -501,7 +370,6 @@ Before considering template complete:
|
|||||||
- [ ] Complex expressions moved to `computed()` in component
|
- [ ] Complex expressions moved to `computed()` in component
|
||||||
- [ ] @defer used for below-the-fold or heavy components
|
- [ ] @defer used for below-the-fold or heavy components
|
||||||
- [ ] Content projection using ng-content with proper selectors (if applicable)
|
- [ ] Content projection using ng-content with proper selectors (if applicable)
|
||||||
- [ ] Template references using # for local variables
|
|
||||||
- [ ] Using @let for template-scoped variables (Angular 18.1+)
|
- [ ] Using @let for template-scoped variables (Angular 18.1+)
|
||||||
|
|
||||||
### E2E Attributes
|
### E2E Attributes
|
||||||
@@ -510,7 +378,6 @@ Before considering template complete:
|
|||||||
- [ ] All links have `data-what` and `data-which`
|
- [ ] All links have `data-what` and `data-which`
|
||||||
- [ ] Dynamic lists use `[attr.data-*]` bindings with unique identifiers
|
- [ ] Dynamic lists use `[attr.data-*]` bindings with unique identifiers
|
||||||
- [ ] No duplicate `data-which` values within the same view
|
- [ ] No duplicate `data-which` values within the same view
|
||||||
- [ ] Additional `data-*` attributes for contextual information (if needed)
|
|
||||||
|
|
||||||
### ARIA Accessibility
|
### ARIA Accessibility
|
||||||
- [ ] All interactive elements have appropriate ARIA labels
|
- [ ] All interactive elements have appropriate ARIA labels
|
||||||
@@ -520,16 +387,13 @@ Before considering template complete:
|
|||||||
- [ ] Dialogs have `role="dialog"`, `aria-modal`, and label relationships
|
- [ ] Dialogs have `role="dialog"`, `aria-modal`, and label relationships
|
||||||
- [ ] Dynamic state changes reflected in ARIA attributes
|
- [ ] Dynamic state changes reflected in ARIA attributes
|
||||||
- [ ] Keyboard accessibility (tabindex, enter/space handlers where needed)
|
- [ ] Keyboard accessibility (tabindex, enter/space handlers where needed)
|
||||||
- [ ] Screen reader testing completed (if applicable)
|
|
||||||
|
|
||||||
### Combined Standards
|
### Combined Standards
|
||||||
- [ ] Every interactive element has BOTH E2E and ARIA attributes
|
- [ ] Every interactive element has BOTH E2E and ARIA attributes
|
||||||
- [ ] Attributes organized logically in template (Angular directives → data-* → aria-*)
|
- [ ] Attributes organized logically (Angular directives → data-* → aria-*)
|
||||||
- [ ] Dynamic bindings use `[attr.*]` syntax correctly
|
- [ ] Dynamic bindings use `[attr.*]` syntax correctly
|
||||||
- [ ] No accessibility violations (semantic HTML preferred over ARIA)
|
- [ ] No accessibility violations (semantic HTML preferred over ARIA)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Guide
|
## Migration Guide
|
||||||
|
|
||||||
### From Legacy Angular Syntax
|
### From Legacy Angular Syntax
|
||||||
@@ -549,31 +413,27 @@ Before considering template complete:
|
|||||||
3. **Add ARIA attributes**: appropriate role, label, and state attributes
|
3. **Add ARIA attributes**: appropriate role, label, and state attributes
|
||||||
4. **Test**: verify selectors work in E2E tests, validate with screen readers
|
4. **Test**: verify selectors work in E2E tests, validate with screen readers
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reference Files
|
## Reference Files
|
||||||
|
|
||||||
For detailed examples and advanced patterns, see:
|
For detailed examples and advanced patterns, see:
|
||||||
|
|
||||||
### Angular Syntax References
|
### Angular Syntax References
|
||||||
- `references/control-flow-reference.md` - @if/@for/@switch patterns
|
- `references/control-flow-reference.md` - Advanced @if/@for/@switch patterns, nested loops, filtering
|
||||||
- `references/defer-patterns.md` - Lazy loading strategies
|
- `references/defer-patterns.md` - Lazy loading strategies, Core Web Vitals, performance optimization
|
||||||
- `references/projection-patterns.md` - Advanced ng-content
|
- `references/projection-patterns.md` - Advanced ng-content, conditional projection, template-based patterns
|
||||||
- `references/template-reference.md` - ng-template/ng-container
|
- `references/template-reference.md` - ng-template/ng-container, programmatic rendering, ViewContainerRef
|
||||||
|
|
||||||
### E2E Testing References
|
### E2E Testing References
|
||||||
- `references/e2e-attributes.md` - Complete E2E attribute patterns and conventions
|
- `references/e2e-attributes.md` - Complete E2E attribute patterns, naming conventions, testing integration
|
||||||
|
|
||||||
### ARIA Accessibility References
|
### ARIA Accessibility References
|
||||||
- `references/aria-attributes.md` - Comprehensive ARIA guidance and WCAG compliance
|
- `references/aria-attributes.md` - Comprehensive ARIA guidance, roles, properties, states, WCAG compliance
|
||||||
|
|
||||||
### Combined References
|
### Combined References
|
||||||
- `references/combined-patterns.md` - Real-world examples with Angular + E2E + ARIA
|
- `references/combined-patterns.md` - Real-world examples with Angular + E2E + ARIA integrated
|
||||||
|
|
||||||
Search with: `grep -r "pattern" references/`
|
Search with: `grep -r "pattern" references/`
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Reference Summary
|
## Quick Reference Summary
|
||||||
|
|
||||||
**Every interactive element needs:**
|
**Every interactive element needs:**
|
||||||
|
|||||||
@@ -1,192 +1,192 @@
|
|||||||
---
|
---
|
||||||
name: test-migration
|
name: test-migration
|
||||||
description: Reference patterns for Jest to Vitest test migration. Contains syntax mappings for jest→vi, Spectator→Angular Testing Library, and common matcher conversions. Auto-loaded by migration-specialist agent.
|
description: Reference patterns for Jest to Vitest test migration. Contains syntax mappings for jest→vi, Spectator→Angular Testing Library, and common matcher conversions. Auto-loaded by migration-specialist agent.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Jest to Vitest Migration Patterns
|
# Jest to Vitest Migration Patterns
|
||||||
|
|
||||||
Quick reference for test framework migration syntax.
|
Quick reference for test framework migration syntax.
|
||||||
|
|
||||||
## Import Changes
|
## Import Changes
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// REMOVE
|
// REMOVE
|
||||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||||
|
|
||||||
// ADD
|
// ADD
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mock Migration
|
## Mock Migration
|
||||||
|
|
||||||
| Jest | Vitest |
|
| Jest | Vitest |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| `jest.fn()` | `vi.fn()` |
|
| `jest.fn()` | `vi.fn()` |
|
||||||
| `jest.spyOn(obj, 'method')` | `vi.spyOn(obj, 'method')` |
|
| `jest.spyOn(obj, 'method')` | `vi.spyOn(obj, 'method')` |
|
||||||
| `jest.mock('module')` | `vi.mock('module')` |
|
| `jest.mock('module')` | `vi.mock('module')` |
|
||||||
| `jest.useFakeTimers()` | `vi.useFakeTimers()` |
|
| `jest.useFakeTimers()` | `vi.useFakeTimers()` |
|
||||||
| `jest.advanceTimersByTime(ms)` | `vi.advanceTimersByTime(ms)` |
|
| `jest.advanceTimersByTime(ms)` | `vi.advanceTimersByTime(ms)` |
|
||||||
| `jest.useRealTimers()` | `vi.useRealTimers()` |
|
| `jest.useRealTimers()` | `vi.useRealTimers()` |
|
||||||
| `jest.clearAllMocks()` | `vi.clearAllMocks()` |
|
| `jest.clearAllMocks()` | `vi.clearAllMocks()` |
|
||||||
| `jest.resetAllMocks()` | `vi.resetAllMocks()` |
|
| `jest.resetAllMocks()` | `vi.resetAllMocks()` |
|
||||||
|
|
||||||
## Spectator → TestBed
|
## Spectator → TestBed
|
||||||
|
|
||||||
### Component Testing
|
### Component Testing
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// OLD (Spectator)
|
// OLD (Spectator)
|
||||||
const createComponent = createComponentFactory({
|
const createComponent = createComponentFactory({
|
||||||
component: MyComponent,
|
component: MyComponent,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
mocks: [MyService]
|
mocks: [MyService]
|
||||||
});
|
});
|
||||||
let spectator: Spectator<MyComponent>;
|
let spectator: Spectator<MyComponent>;
|
||||||
beforeEach(() => spectator = createComponent());
|
beforeEach(() => spectator = createComponent());
|
||||||
|
|
||||||
// NEW (TestBed)
|
// NEW (TestBed)
|
||||||
let fixture: ComponentFixture<MyComponent>;
|
let fixture: ComponentFixture<MyComponent>;
|
||||||
let component: MyComponent;
|
let component: MyComponent;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [MyComponent],
|
imports: [MyComponent],
|
||||||
providers: [{ provide: MyService, useValue: mockService }]
|
providers: [{ provide: MyService, useValue: mockService }]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
fixture = TestBed.createComponent(MyComponent);
|
fixture = TestBed.createComponent(MyComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service Testing
|
### Service Testing
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// OLD (Spectator)
|
// OLD (Spectator)
|
||||||
const createService = createServiceFactory({
|
const createService = createServiceFactory({
|
||||||
service: MyService,
|
service: MyService,
|
||||||
mocks: [HttpClient]
|
mocks: [HttpClient]
|
||||||
});
|
});
|
||||||
let spectator: SpectatorService<MyService>;
|
let spectator: SpectatorService<MyService>;
|
||||||
beforeEach(() => spectator = createService());
|
beforeEach(() => spectator = createService());
|
||||||
|
|
||||||
// NEW (TestBed)
|
// NEW (TestBed)
|
||||||
let service: MyService;
|
let service: MyService;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
MyService,
|
MyService,
|
||||||
{ provide: HttpClient, useValue: mockHttp }
|
{ provide: HttpClient, useValue: mockHttp }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
service = TestBed.inject(MyService);
|
service = TestBed.inject(MyService);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Query Selectors
|
## Query Selectors
|
||||||
|
|
||||||
| Spectator | Angular/Native |
|
| Spectator | Angular/Native |
|
||||||
|-----------|---------------|
|
|-----------|---------------|
|
||||||
| `spectator.query('.class')` | `fixture.nativeElement.querySelector('.class')` |
|
| `spectator.query('.class')` | `fixture.nativeElement.querySelector('.class')` |
|
||||||
| `spectator.queryAll('.class')` | `fixture.nativeElement.querySelectorAll('.class')` |
|
| `spectator.queryAll('.class')` | `fixture.nativeElement.querySelectorAll('.class')` |
|
||||||
| `spectator.query('button')` | `fixture.nativeElement.querySelector('button')` |
|
| `spectator.query('button')` | `fixture.nativeElement.querySelector('button')` |
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
| Spectator | Native |
|
| Spectator | Native |
|
||||||
|-----------|--------|
|
|-----------|--------|
|
||||||
| `spectator.click(element)` | `element.click()` + `fixture.detectChanges()` |
|
| `spectator.click(element)` | `element.click()` + `fixture.detectChanges()` |
|
||||||
| `spectator.typeInElement(value, el)` | `el.value = value; el.dispatchEvent(new Event('input'))` |
|
| `spectator.typeInElement(value, el)` | `el.value = value; el.dispatchEvent(new Event('input'))` |
|
||||||
| `spectator.blur(element)` | `element.dispatchEvent(new Event('blur'))` |
|
| `spectator.blur(element)` | `element.dispatchEvent(new Event('blur'))` |
|
||||||
|
|
||||||
## Matchers
|
## Matchers
|
||||||
|
|
||||||
| Spectator | Standard |
|
| Spectator | Standard |
|
||||||
|-----------|----------|
|
|-----------|----------|
|
||||||
| `toHaveText('text')` | `expect(el.textContent).toContain('text')` |
|
| `toHaveText('text')` | `expect(el.textContent).toContain('text')` |
|
||||||
| `toExist()` | `toBeTruthy()` |
|
| `toExist()` | `toBeTruthy()` |
|
||||||
| `toBeVisible()` | Check `!el.hidden && el.offsetParent !== null` |
|
| `toBeVisible()` | Check `!el.hidden && el.offsetParent !== null` |
|
||||||
| `toHaveClass('class')` | `expect(el.classList.contains('class')).toBe(true)` |
|
| `toHaveClass('class')` | `expect(el.classList.contains('class')).toBe(true)` |
|
||||||
|
|
||||||
## Async Patterns
|
## Async Patterns
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// OLD (callback)
|
// OLD (callback)
|
||||||
it('should emit', (done) => {
|
it('should emit', (done) => {
|
||||||
service.data$.subscribe(val => {
|
service.data$.subscribe(val => {
|
||||||
expect(val).toBe(expected);
|
expect(val).toBe(expected);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// NEW (async/await)
|
// NEW (async/await)
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
it('should emit', async () => {
|
it('should emit', async () => {
|
||||||
const val = await firstValueFrom(service.data$);
|
const val = await firstValueFrom(service.data$);
|
||||||
expect(val).toBe(expected);
|
expect(val).toBe(expected);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Input/Output Testing
|
## Input/Output Testing
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Setting inputs (Angular 17.3+)
|
// Setting inputs (Angular 17.3+)
|
||||||
fixture.componentRef.setInput('title', 'Test');
|
fixture.componentRef.setInput('title', 'Test');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
// Testing outputs
|
// Testing outputs
|
||||||
const emitSpy = vi.fn();
|
const emitSpy = vi.fn();
|
||||||
component.myOutput.subscribe(emitSpy);
|
component.myOutput.subscribe(emitSpy);
|
||||||
// trigger action...
|
// trigger action...
|
||||||
expect(emitSpy).toHaveBeenCalledWith(expectedValue);
|
expect(emitSpy).toHaveBeenCalledWith(expectedValue);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
|
|
||||||
### vite.config.mts Template
|
### vite.config.mts Template
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
/// <reference types='vitest' />
|
/// <reference types='vitest' />
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import angular from '@analogjs/vite-plugin-angular';
|
import angular from '@analogjs/vite-plugin-angular';
|
||||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||||
|
|
||||||
export default
|
export default
|
||||||
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
|
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
|
||||||
defineConfig(() => ({
|
defineConfig(() => ({
|
||||||
root: __dirname,
|
root: __dirname,
|
||||||
cacheDir: '../../../node_modules/.vite/libs/[path]', // Adjust depth!
|
cacheDir: '../../../node_modules/.vite/libs/[path]', // Adjust depth!
|
||||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||||
test: {
|
test: {
|
||||||
watch: false,
|
watch: false,
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||||
setupFiles: ['src/test-setup.ts'],
|
setupFiles: ['src/test-setup.ts'],
|
||||||
reporters: [
|
reporters: [
|
||||||
'default',
|
'default',
|
||||||
['junit', { outputFile: '../../../testresults/junit-[name].xml' }],
|
['junit', { outputFile: '../../../testresults/junit-[name].xml' }],
|
||||||
],
|
],
|
||||||
coverage: {
|
coverage: {
|
||||||
reportsDirectory: '../../../coverage/libs/[path]',
|
reportsDirectory: '../../../coverage/libs/[path]',
|
||||||
provider: 'v8' as const,
|
provider: 'v8' as const,
|
||||||
reporter: ['text', 'cobertura'],
|
reporter: ['text', 'cobertura'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
```
|
```
|
||||||
|
|
||||||
### test-setup.ts
|
### test-setup.ts
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import '@analogjs/vitest-angular/setup-zone';
|
import '@analogjs/vitest-angular/setup-zone';
|
||||||
```
|
```
|
||||||
|
|
||||||
## Path Depth Reference
|
## Path Depth Reference
|
||||||
|
|
||||||
| Library Location | Relative Path Prefix |
|
| Library Location | Relative Path Prefix |
|
||||||
|-----------------|---------------------|
|
|-----------------|---------------------|
|
||||||
| `libs/feature/ui` | `../../` |
|
| `libs/feature/ui` | `../../` |
|
||||||
| `libs/feature/data-access` | `../../` |
|
| `libs/feature/data-access` | `../../` |
|
||||||
| `libs/domain/feature/ui` | `../../../` |
|
| `libs/domain/feature/ui` | `../../../` |
|
||||||
| `libs/domain/feature/data-access/store` | `../../../../` |
|
| `libs/domain/feature/data-access/store` | `../../../../` |
|
||||||
|
|||||||
Reference in New Issue
Block a user