diff --git a/docs/AST_VALIDATION_GUIDE.md b/docs/AST_VALIDATION_GUIDE.md new file mode 100644 index 00000000..8c432382 --- /dev/null +++ b/docs/AST_VALIDATION_GUIDE.md @@ -0,0 +1,298 @@ +# AST-based Validation Suite + +This comprehensive validation suite uses Abstract Syntax Tree (AST) analysis to enforce code quality standards across all packages in the devlog monorepo. It helps AI agents and developers maintain consistent, high-quality code by catching issues early. + +## šŸŽÆ Overview + +The validation suite includes 5 specialized AST-based validation scripts that analyze TypeScript code for: + +- **TypeScript Best Practices** - Type safety, async patterns, error handling +- **Architecture Patterns** - Manager classes, interfaces, service layers +- **Testing Standards** - Test structure, isolation, async patterns +- **Security & Performance** - Security vulnerabilities, performance anti-patterns +- **Import Patterns** - ESM imports, cross-package dependencies + +## šŸš€ Quick Start + +### Run All Validations +```bash +npm run validate +# or +node scripts/validate-all-ast.js +``` + +### Run Specific Validation +```bash +npm run validate:typescript +npm run validate:architecture +npm run validate:testing +npm run validate:security +npm run validate:imports +``` + +### Quick Mode (Skip Build/Type Checks) +```bash +npm run validate:quick +# or +node scripts/validate-all-ast.js --quick +``` + +## šŸ“‹ Available Commands + +| Command | Description | +|---------|-------------| +| `npm run validate` | Run complete validation suite | +| `npm run validate:quick` | Quick validation (no build/types) | +| `npm run validate:list` | List all available validations | +| `npm run validate:typescript` | TypeScript best practices | +| `npm run validate:architecture` | Architecture patterns | +| `npm run validate:testing` | Testing standards | +| `npm run validate:security` | Security & performance | +| `npm run validate:imports` | Import patterns | + +### Orchestrator Options + +```bash +# Show help +node scripts/validate-all-ast.js --help + +# List available scripts +node scripts/validate-all-ast.js --list + +# Run specific script +node scripts/validate-all-ast.js --script typescript + +# Skip build validation (faster) +node scripts/validate-all-ast.js --no-build + +# Skip TypeScript compilation check +node scripts/validate-all-ast.js --no-types + +# Quick mode (skip both) +node scripts/validate-all-ast.js --quick +``` + +## šŸ” Validation Details + +### 1. TypeScript Best Practices (`validate-typescript-best-practices-ast.js`) + +**Checks for:** +- āŒ Usage of `any` type (warnings) +- āŒ Non-null assertion operator overuse +- āŒ Unsafe type assertions +- āŒ Missing await on async methods +- āŒ Async functions without error handling +- āŒ Empty catch blocks (errors) +- āŒ Throwing strings instead of Error objects +- āŒ Missing JSDoc on exported APIs +- āŒ Unconstrained generic types + +**Example Issues Caught:** +```typescript +// āŒ Will warn about 'any' usage +function process(data: any) { ... } + +// āŒ Will error on empty catch +try { ... } catch (e) { } + +// āŒ Will warn about missing await +async function example() { + manager.initialize(); // Should be: await manager.initialize() +} +``` + +### 2. Architecture Patterns (`validate-architecture-patterns-ast.js`) + +**Checks for:** +- āŒ Manager classes missing `initialize()`/`dispose()` methods (errors) +- āŒ Manager methods should be async +- āŒ Missing dependency injection in constructors +- āŒ Interfaces not prefixed with 'I' +- āŒ Service classes with mutable state +- āŒ Storage providers not extending base classes +- āŒ Package-specific patterns (MCP tools, React components, etc.) + +**Example Issues Caught:** +```typescript +// āŒ Manager missing required methods +export class UserManager { + constructor(private storage: Storage) {} // āœ… Has DI + // āŒ Missing initialize() and dispose() methods +} + +// āŒ Interface naming +interface StorageProvider { ... } // Should be: IStorageProvider +``` + +### 3. Testing Standards (`validate-testing-standards-ast.js`) + +**Checks for:** +- āŒ Test files missing testing framework imports (errors) +- āŒ Tests without assertions +- āŒ Missing test isolation (cleanup in afterEach) +- āŒ Async tests without proper await patterns +- āŒ File system operations without temporary directories +- āŒ Mock usage without cleanup +- āŒ Poor test naming (should start with "should") + +**Example Issues Caught:** +```typescript +// āŒ Missing framework import +describe('Component', () => { ... }); // Missing: import { describe, it } from 'vitest' + +// āŒ Test without assertion +it('should work', async () => { + await component.doSomething(); + // āŒ No expect() call +}); + +// āŒ File operations without cleanup +beforeEach(() => { + fs.writeFileSync('test.txt', 'data'); // āŒ Should use temp directories +}); +``` + +### 4. Security & Performance (`validate-security-performance-ast.js`) + +**Security Checks:** +- āŒ XSS vulnerabilities (`innerHTML` usage) (errors) +- āŒ `eval()` usage (errors) +- āŒ Hardcoded secrets/credentials (errors) +- āŒ SQL injection patterns (errors) +- āŒ Dangerous regex patterns (ReDoS) (warnings) +- āŒ Path traversal vulnerabilities (warnings) + +**Performance Checks:** +- āŒ Synchronous blocking operations (warnings) +- āŒ Inefficient nested loops (warnings) +- āŒ Memory leaks (timer cleanup, event listeners) (warnings) +- āŒ Inefficient array operations (warnings) +- āŒ Large object literals (warnings) + +**Example Issues Caught:** +```typescript +// āŒ Security: XSS vulnerability +element.innerHTML = userInput; // Should sanitize or use textContent + +// āŒ Security: Hardcoded secret +const apiKey = "sk-1234567890abcdef"; // Should use environment variables + +// āŒ Performance: Sync operation +const data = fs.readFileSync('file.txt'); // Should use async version + +// āŒ Performance: Timer leak +setInterval(() => { ... }, 1000); // Missing clearInterval in cleanup +``` + +### 5. Import Patterns (`validate-imports.js`) + +**Checks for:** +- āŒ Missing `.js` extensions on relative imports (errors) +- āŒ Self-referencing `@/` aliases (outside web package) (errors) +- āŒ Cross-package relative imports (errors) +- āŒ Invalid package names in cross-package imports (errors) + +**Example Issues Caught:** +```typescript +// āŒ Missing .js extension +import { Helper } from './helper'; // Should be: './helper.js' + +// āŒ Cross-package relative import +import { CoreType } from '../../core/src/types'; // Should be: '@devlog/core' + +// āŒ Self-referencing alias (outside web package) +import { Utils } from '@/utils'; // Should use relative: './utils.js' +``` + +## šŸ“Š Output Format + +The validation scripts provide detailed, actionable feedback: + +``` +āŒ Found 2 TypeScript best practice errors: + +šŸ“ packages/core/src/manager.ts:45 [EMPTY_CATCH_BLOCK] + Empty catch block - errors should be handled + šŸ’” Add error logging, re-throwing, or proper error handling + +šŸ“ packages/web/components/Button.tsx:12 [MISSING_JSDOC] + Exported function "Button" missing JSDoc documentation + šŸ’” Add JSDoc comments for public API documentation +``` + +## šŸŽÆ Exit Codes + +- **0**: All validations passed (or warnings only) +- **1**: Critical errors found (must be fixed) + +## šŸ› ļø Integration + +### Pre-commit Hook +The validation is integrated into the pre-commit hook: +```json +"pre-commit": "lint-staged && node scripts/validate-imports.js" +``` + +### CI/CD Integration +Add to your CI pipeline: +```yaml +- name: Validate Code Quality + run: npm run validate +``` + +### Development Workflow +```bash +# Before committing +npm run validate:quick + +# Full validation before PR +npm run validate + +# Fix specific issues +npm run validate:typescript +``` + +## šŸ“ˆ Benefits + +### For AI Agents +- **Consistent Patterns**: Enforces architectural patterns AI can rely on +- **Early Error Detection**: Catches issues before they compound +- **Code Quality Gates**: Prevents degradation during automated changes +- **Contextual Guidance**: Provides specific suggestions for fixes + +### For Developers +- **Code Reviews**: Automated checks reduce manual review overhead +- **Learning Tool**: Warnings teach best practices +- **Refactoring Safety**: Catches regressions during code changes +- **Documentation**: Enforces API documentation standards + +### For Project Maintenance +- **Consistency**: Uniform code style across all packages +- **Security**: Proactive security vulnerability detection +- **Performance**: Early performance anti-pattern detection +- **Scalability**: Maintains quality as codebase grows + +## šŸ”§ Customization + +### Adding New Validations + +1. Create new validation script in `scripts/` directory +2. Follow naming pattern: `validate-{name}-ast.js` +3. Export main function for orchestrator integration +4. Add to `package.json` scripts section + +### Modifying Validation Rules + +Each validation script contains clearly marked sections for different types of checks. Modify the validation logic in the specific `validate*` functions within each script. + +### Package-Specific Rules + +The architecture validation includes package-specific checks: +- **Core**: Manager exports, type exports +- **MCP**: Tool patterns, error handling +- **Web**: React patterns, Next.js routes +- **AI**: Parser implementations + +## šŸŽ Summary + +This AST-based validation suite provides comprehensive code quality enforcement that helps maintain a high-quality, consistent codebase. It's designed to work seamlessly with AI agents while providing valuable feedback to human developers. The modular design allows for easy extension and customization as the project evolves. \ No newline at end of file diff --git a/docs/DYNAMIC_VALIDATION_OVERHAUL.md b/docs/DYNAMIC_VALIDATION_OVERHAUL.md new file mode 100644 index 00000000..78ad2571 --- /dev/null +++ b/docs/DYNAMIC_VALIDATION_OVERHAUL.md @@ -0,0 +1,113 @@ +# Architecture Validation Overhaul - Dynamic Pattern Discovery + +## šŸŽÆ Problem Addressed + +The validation scripts were using hardcoded assumptions about manager classes and architectural patterns that didn't match the current codebase: + +1. **Hardcoded expectations**: Scripts assumed all managers should have `dispose()` methods, but some actually use `cleanup()` +2. **No adaptation**: Scripts enforced rigid rules instead of discovering actual patterns +3. **Inconsistent with reality**: The validation flagged legitimate code as errors + +## šŸ”§ Solution Implemented + +### Dynamic Pattern Discovery + +The new validation system: + +1. **Discovers actual patterns** used in the codebase instead of enforcing hardcoded rules +2. **Categorizes managers** by type (Workspace, Storage, Other) for context-aware validation +3. **Validates consistency** within each category rather than applying global rules +4. **Adapts to the architecture** as it evolves + +### Key Improvements + +#### Before (Hardcoded) +```javascript +// Old: Hardcoded assumptions +if (!hasDisposeMethod) { + ERRORS.push({ + message: `Manager class "${className}" missing dispose() method`, + // Always required dispose(), even if cleanup() was used + }); +} +``` + +#### After (Dynamic) +```javascript +// New: Discovers actual patterns +const primaryCleanupPattern = [...categoryCleanupPatterns.entries()] + .sort((a, b) => b[1] - a[1])[0]; + +// Validates consistency within discovered patterns +if (primaryCleanupPattern && pattern !== primaryCleanupPattern[0]) { + WARNINGS.push({ + message: `${categoryName} manager uses ${pattern} but category standard is ${primaryCleanupPattern[0]}`, + // Suggests consistency based on what's actually used + }); +} +``` + +## šŸ“Š Results + +### Before the Overhaul +``` +āŒ Found 6 architecture pattern errors: +- WorkspaceDevlogManager missing dispose() method (but it has cleanup()!) +- ConfigurationManager missing dispose() method +- FileWorkspaceManager missing initialize() and dispose() methods +- GitHubLabelManager missing initialize() and dispose() methods +``` + +### After the Overhaul +``` +šŸŽÆ Manager Categories: + Workspace managers: 4 + Storage managers: 1 + Other managers: 1 + +šŸ” Cleanup patterns found: + - cleanup:true: 1 classes + - dispose:true: 2 classes + +āŒ Found 2 consistency errors: +- FileWorkspaceManager missing initialize() method (legitimate issue) +- FileWorkspaceManager missing cleanup method (legitimate issue) + +āš ļø Found 1 consistency warning: +- WorkspaceDevlogManager uses cleanup:true but category standard is dispose:true + (This is a legitimate inconsistency worth reviewing) +``` + +## šŸŽ‰ Benefits + +1. **Accurate Validation**: Only flags actual inconsistencies and missing patterns +2. **Adaptive**: Works with the current architecture instead of enforcing old patterns +3. **Context-Aware**: Different manager types can have different valid patterns +4. **Informative**: Shows what patterns are actually used in the codebase +5. **Non-Disruptive**: Doesn't require changing working code to match arbitrary rules + +## šŸ” Discovery Output + +The new validation provides insights into the actual codebase: + +``` +šŸ“Š Pattern Discovery Summary: + Manager classes: 6 + Service classes: 2 + Provider classes: 4 + Initialization methods: initialize + Cleanup methods: cleanup, dispose +``` + +This helps developers understand the actual architectural patterns in use rather than assuming what they should be. + +## šŸš€ Next Steps + +The dynamic validation system can be extended to: + +1. **Detect new patterns** as the architecture evolves +2. **Suggest migrations** when patterns become inconsistent +3. **Validate cross-package** consistency automatically +4. **Generate documentation** about actual architectural patterns + +This approach ensures the validation system remains useful and accurate as the codebase grows and evolves. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..98c85fe7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1510 @@ +{ + "name": "devlog", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "devlog", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "better-sqlite3": "^11.10.0", + "dotenv": "16.5.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "concurrently": "9.2.0", + "husky": "9.1.7", + "lint-staged": "16.1.2", + "prettier": "3.6.1", + "typescript": "5.8.3" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", + "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/concurrently": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lint-staged": { + "version": "16.1.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.2.tgz", + "integrity": "sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0", + "debug": "^4.4.1", + "lilconfig": "^3.1.3", + "listr2": "^8.3.3", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nano-spawn": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", + "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prettier": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz", + "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json index 24ac7256..3d7e4abd 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,13 @@ "start:web": "pnpm --filter @codervisor/devlog-web start", "preview:web": "pnpm --filter @codervisor/devlog-web preview", "format": "prettier --write packages/**/*.{ts,tsx,js,jsx,json,md}", - "validate": "node scripts/validation/validate-all.js", + "validate": "node scripts/validate-all-ast.js", + "validate:typescript": "node scripts/validate-typescript-best-practices-ast.js", + "validate:architecture": "node scripts/validate-architecture-patterns-ast.js", + "validate:testing": "node scripts/validate-testing-standards-ast.js", + "validate:security": "node scripts/validate-security-performance-ast.js", + "validate:quick": "node scripts/validate-all-ast.js --quick", + "validate:list": "node scripts/validate-all-ast.js --list", "validate:imports": "node scripts/validation/validate-imports.js", "validate:api": "node scripts/validation/validate-api-standardization-ast.js", "validate:envelopes": "node scripts/validation/validate-response-envelopes-ast.js", @@ -66,6 +72,5 @@ "better-sqlite3": "^11.10.0", "dotenv": "16.5.0", "tsx": "^4.0.0" - }, - "packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad" + } } diff --git a/scripts/validate-all-ast.js b/scripts/validate-all-ast.js new file mode 100755 index 00000000..74307e28 --- /dev/null +++ b/scripts/validate-all-ast.js @@ -0,0 +1,267 @@ +#!/usr/bin/env node + +/** + * Comprehensive AST-based Validation Suite + * Orchestrates all validation scripts for complete code quality assessment + */ + +const { execSync } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +let totalErrors = 0; +let totalWarnings = 0; + +/** + * Run a validation script with error counting + */ +function runValidation(name, scriptPath, args = []) { + console.log(`\nšŸ” Running ${name}...`); + console.log('='.repeat(50)); + + const startTime = Date.now(); + + try { + const command = `node ${scriptPath} ${args.join(' ')}`; + execSync(command, { stdio: 'inherit' }); + + const duration = ((Date.now() - startTime) / 1000).toFixed(2); + console.log(`āœ… ${name} completed successfully (${duration}s)`); + } catch (error) { + const duration = ((Date.now() - startTime) / 1000).toFixed(2); + if (error.status === 1) { + totalErrors++; + console.log(`āŒ ${name} failed with errors (${duration}s)`); + } else { + console.log(`āš ļø ${name} completed with warnings (${duration}s)`); + } + } +} + +/** + * Run TypeScript compilation check + */ +function runTypeCheck() { + console.log(`\nšŸ” Running TypeScript Compilation Check...`); + console.log('='.repeat(50)); + + try { + // Check each package's TypeScript + const packages = ['core', 'web', 'mcp', 'ai']; + + packages.forEach(pkg => { + try { + console.log(` Checking @devlog/${pkg}...`); + execSync(`pnpm --filter @devlog/${pkg} type-check 2>/dev/null || pnpm --filter @devlog/${pkg} tsc --noEmit`, { stdio: 'pipe' }); + } catch (error) { + console.log(` āš ļø TypeScript issues in ${pkg} package`); + totalWarnings++; + } + }); + + console.log(`āœ… TypeScript Compilation Check completed`); + } catch (error) { + console.log(`āŒ TypeScript Compilation Check failed`); + totalErrors++; + } +} + +/** + * Run build validation + */ +function runBuildValidation() { + console.log(`\nšŸ” Running Build Validation...`); + console.log('='.repeat(50)); + + try { + // Test build without affecting dev servers + console.log(` Testing web package build...`); + execSync('pnpm --filter @devlog/web build:test', { stdio: 'pipe' }); + + console.log(`āœ… Build Validation completed successfully`); + } catch (error) { + console.log(`āŒ Build Validation failed`); + totalErrors++; + } +} + +/** + * Get validation scripts from the scripts directory + */ +function getValidationScripts() { + const scriptsDir = path.join(__dirname); + const scripts = []; + + // Find all validation scripts + const files = fs.readdirSync(scriptsDir); + + files.forEach(file => { + if (file.startsWith('validate-') && file.endsWith('.js') && file !== 'validate-all-ast.js') { + const scriptPath = path.join(scriptsDir, file); + const name = file.replace('validate-', '').replace('-ast.js', '').replace('.js', '').replace(/-/g, ' '); + const displayName = name.charAt(0).toUpperCase() + name.slice(1); + + scripts.push({ + name: displayName, + scriptPath, + fileName: file + }); + } + }); + + return scripts; +} + +/** + * Main validation suite + */ +function runFullValidation() { + console.log('šŸš€ Starting Comprehensive AST-based Validation Suite'); + console.log('=' .repeat(60)); + + const startTime = Date.now(); + + // Get all validation scripts + const validationScripts = getValidationScripts(); + + console.log(`Found ${validationScripts.length} validation scripts to run:`); + validationScripts.forEach(script => { + console.log(` • ${script.name} (${script.fileName})`); + }); + + // Run all AST-based validations + validationScripts.forEach(script => { + runValidation(script.name, script.scriptPath); + }); + + // Run additional system checks + runTypeCheck(); + runBuildValidation(); + + // Summary report + const endTime = Date.now(); + const duration = ((endTime - startTime) / 1000).toFixed(2); + + console.log('\nšŸŽÆ Comprehensive Validation Summary'); + console.log('=' .repeat(60)); + console.log(`ā±ļø Duration: ${duration}s`); + console.log(`šŸ” Scripts run: ${validationScripts.length + 2}`); + console.log(`āŒ Errors: ${totalErrors}`); + console.log(`āš ļø Warnings: ${totalWarnings}`); + + if (totalErrors === 0 && totalWarnings === 0) { + console.log('\nšŸŽ‰ All validations passed! Code quality is excellent.'); + console.log('✨ Your codebase follows all established patterns and standards.'); + } else if (totalErrors === 0) { + console.log('\nāœ… No critical errors found! Code is ready for production.'); + console.log('šŸ’” Consider addressing warnings to further improve code quality.'); + } else { + console.log('\n🚨 Critical errors found! Please fix before deploying.'); + console.log(` ${totalErrors} error(s) must be resolved.`); + console.log('šŸ”§ Run individual validation scripts for detailed error information.'); + } + + // Exit with appropriate code + process.exit(totalErrors > 0 ? 1 : 0); +} + +/** + * Display help information + */ +function showHelp() { + const validationScripts = getValidationScripts(); + + console.log(` +šŸ› ļø Comprehensive AST-based Validation Suite for Devlog Project + +Usage: node scripts/validate-all-ast.js [options] + +Options: + --help, -h Show this help message + --list List all available validation scripts + --script Run only a specific validation script + --no-build Skip build validation (faster) + --no-types Skip TypeScript compilation check + --quick Run only critical validations (no-build + no-types) + +Available Validation Scripts: +${validationScripts.map(s => ` • ${s.name.padEnd(25)} - ${s.fileName}`).join('\n')} + +Examples: + node scripts/validate-all-ast.js # Run all validations + node scripts/validate-all-ast.js --script imports # Run only import validation + node scripts/validate-all-ast.js --quick # Quick validation (skip build/types) + node scripts/validate-all-ast.js --no-build # Skip build check +`); +} + +/** + * Run a specific validation script + */ +function runSpecificScript(scriptName) { + const validationScripts = getValidationScripts(); + const script = validationScripts.find(s => + s.name.toLowerCase().includes(scriptName.toLowerCase()) || + s.fileName.includes(scriptName.toLowerCase()) + ); + + if (!script) { + console.error(`āŒ Unknown validation script: ${scriptName}`); + console.log(`Available scripts: ${validationScripts.map(s => s.name).join(', ')}`); + process.exit(1); + } + + console.log(`šŸš€ Running specific validation: ${script.name}`); + console.log('=' .repeat(50)); + + runValidation(script.name, script.scriptPath); + + if (totalErrors === 0) { + console.log('\nāœ… Validation completed successfully!'); + } else { + console.log('\nāŒ Validation found errors that need to be fixed.'); + } + + process.exit(totalErrors > 0 ? 1 : 0); +} + +// Command line argument parsing +const args = process.argv.slice(2); + +if (args.includes('--help') || args.includes('-h')) { + showHelp(); + process.exit(0); +} + +if (args.includes('--list')) { + const validationScripts = getValidationScripts(); + console.log('šŸ“‹ Available Validation Scripts:'); + validationScripts.forEach(script => { + console.log(` • ${script.name} - ${script.fileName}`); + }); + process.exit(0); +} + +// Handle specific script execution +const scriptIndex = args.indexOf('--script'); +if (scriptIndex !== -1 && args[scriptIndex + 1]) { + const scriptName = args[scriptIndex + 1]; + runSpecificScript(scriptName); + return; +} + +// Handle quick mode +if (args.includes('--quick')) { + args.push('--no-build', '--no-types'); +} + +// Handle build/type checking options +if (args.includes('--no-build')) { + runBuildValidation = () => console.log('ā­ļø Skipping build validation'); +} +if (args.includes('--no-types')) { + runTypeCheck = () => console.log('ā­ļø Skipping TypeScript check'); +} + +// Run the full validation suite +runFullValidation(); \ No newline at end of file diff --git a/scripts/validate-architecture-patterns-ast.js b/scripts/validate-architecture-patterns-ast.js new file mode 100755 index 00000000..da829b62 --- /dev/null +++ b/scripts/validate-architecture-patterns-ast.js @@ -0,0 +1,527 @@ +#!/usr/bin/env node + +/** + * Dynamic Architecture Patterns Validation (AST-based) + * + * This validation script dynamically discovers and validates architectural patterns + * in the codebase rather than enforcing hardcoded assumptions. + * + * Key improvements: + * - Discovers actual patterns used in the codebase + * - Validates consistency rather than enforcing rigid rules + * - Adaptable to different architectural styles + * - No hardcoded class names or method expectations + */ + +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); + +const ERRORS = []; +const WARNINGS = []; +const DISCOVERED_PATTERNS = { + managerClasses: new Map(), // className -> { hasInitialize, hasDispose, hasCleanup, constructorParams } + serviceClasses: new Map(), + providerClasses: new Map(), + lifecycleMethods: new Set(), // All cleanup methods found (dispose, cleanup, destroy, etc.) + initializationMethods: new Set(), // All init methods found (initialize, init, setup, etc.) +}; + +/** + * Create TypeScript program for AST analysis + */ +function createProgram(filePaths) { + const compilerOptions = { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + allowJs: true, + jsx: ts.JsxEmit.ReactJSX, + strict: false, + skipLibCheck: true, + skipDefaultLibCheck: true, + noEmit: true, + allowSyntheticDefaultImports: true, + esModuleInterop: true, + }; + + const host = ts.createCompilerHost(compilerOptions); + return ts.createProgram(filePaths, compilerOptions, host); +} + +/** + * Visit AST nodes recursively with proper error handling + */ +function visitNode(node, sourceFile, visitor) { + try { + visitor(node, sourceFile); + node.forEachChild(child => visitNode(child, sourceFile, visitor)); + } catch (error) { + console.warn(`āš ļø Skipping problematic node in ${sourceFile.fileName}: ${error.message}`); + } +} + +/** + * Get line number from AST node with error handling + */ +function getLineNumber(sourceFile, node) { + try { + if (!sourceFile || !node) return 1; + const lineAndChar = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); + return lineAndChar.line + 1; + } catch (error) { + return 1; + } +} + +/** + * Dynamic pattern detection - discover what patterns are actually used + */ +function discoverPatterns(filePath, sourceFile) { + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + // Discover class patterns + if (ts.isClassDeclaration(node) && node.name) { + const className = node.name.text; + const classInfo = { + filePath, + lineNumber: getLineNumber(sourceFile, node), + hasInitialize: false, + hasCleanup: false, + hasDispose: false, + initMethods: [], + cleanupMethods: [], + constructorParams: 0, + isExported: false, + isAbstract: false, + extendsClass: null, + implementsInterfaces: [], + }; + + // Check modifiers + if (node.modifiers) { + classInfo.isExported = node.modifiers.some(m => m.kind === ts.SyntaxKind.ExportKeyword); + classInfo.isAbstract = node.modifiers.some(m => m.kind === ts.SyntaxKind.AbstractKeyword); + } + + // Check inheritance + if (node.heritageClauses) { + node.heritageClauses.forEach(clause => { + if (clause.token === ts.SyntaxKind.ExtendsKeyword) { + clause.types.forEach(type => { + if (ts.isIdentifier(type.expression)) { + classInfo.extendsClass = type.expression.text; + } + }); + } else if (clause.token === ts.SyntaxKind.ImplementsKeyword) { + clause.types.forEach(type => { + if (ts.isIdentifier(type.expression)) { + classInfo.implementsInterfaces.push(type.expression.text); + } + }); + } + }); + } + + // Analyze constructor and methods + node.members.forEach(member => { + if (ts.isConstructorDeclaration(member)) { + classInfo.constructorParams = member.parameters ? member.parameters.length : 0; + } else if (ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name)) { + const methodName = member.name.text; + const isAsync = member.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) || false; + + // Discover initialization patterns + if (['initialize', 'init', 'setup', 'start'].includes(methodName)) { + classInfo.initMethods.push({ name: methodName, isAsync }); + DISCOVERED_PATTERNS.initializationMethods.add(methodName); + if (methodName === 'initialize') classInfo.hasInitialize = true; + } + + // Discover cleanup patterns + if (['dispose', 'cleanup', 'destroy', 'close', 'shutdown', 'stop'].includes(methodName)) { + classInfo.cleanupMethods.push({ name: methodName, isAsync }); + DISCOVERED_PATTERNS.lifecycleMethods.add(methodName); + if (methodName === 'dispose') classInfo.hasDispose = true; + if (methodName === 'cleanup') classInfo.hasCleanup = true; + } + } + }); + + // Categorize classes by naming patterns + if (className.endsWith('Manager') || className.includes('Manager')) { + DISCOVERED_PATTERNS.managerClasses.set(className, classInfo); + } else if (className.endsWith('Service') || className.includes('Service')) { + DISCOVERED_PATTERNS.serviceClasses.set(className, classInfo); + } else if (className.endsWith('Provider') || className.includes('Provider')) { + DISCOVERED_PATTERNS.providerClasses.set(className, classInfo); + } + } + }); +} + +/** + * Analyze discovered patterns and identify inconsistencies + */ +function validatePatternConsistency() { + // Analyze manager patterns + const managerClasses = Array.from(DISCOVERED_PATTERNS.managerClasses.values()); + if (managerClasses.length > 0) { + console.log(`šŸ“Š Discovered ${managerClasses.length} manager classes`); + + // Find common initialization patterns + const initPatterns = new Map(); + const cleanupPatterns = new Map(); + + managerClasses.forEach(manager => { + manager.initMethods.forEach(method => { + const key = `${method.name}:${method.isAsync}`; + initPatterns.set(key, (initPatterns.get(key) || 0) + 1); + }); + + manager.cleanupMethods.forEach(method => { + const key = `${method.name}:${method.isAsync}`; + cleanupPatterns.set(key, (cleanupPatterns.get(key) || 0) + 1); + }); + }); + + console.log(`šŸ” Initialization patterns found:`); + for (const [pattern, count] of initPatterns.entries()) { + console.log(` - ${pattern}: ${count} classes`); + } + + console.log(`šŸ” Cleanup patterns found:`); + for (const [pattern, count] of cleanupPatterns.entries()) { + console.log(` - ${pattern}: ${count} classes`); + } + + // Validate consistency within discovered patterns + validateManagerConsistency(managerClasses, initPatterns, cleanupPatterns); + } + + // Analyze service patterns + const serviceClasses = Array.from(DISCOVERED_PATTERNS.serviceClasses.values()); + if (serviceClasses.length > 0) { + console.log(`šŸ“Š Discovered ${serviceClasses.length} service classes`); + validateServiceConsistency(serviceClasses); + } + + // Analyze provider patterns + const providerClasses = Array.from(DISCOVERED_PATTERNS.providerClasses.values()); + if (providerClasses.length > 0) { + console.log(`šŸ“Š Discovered ${providerClasses.length} provider classes`); + validateProviderConsistency(providerClasses); + } +} + +/** + * Validate consistency among manager classes based on their category + */ +function validateManagerConsistency(managers, initPatterns, cleanupPatterns) { + // Categorize managers by type based on their class names and interfaces + const workspaceManagers = managers.filter(m => { + const className = [...DISCOVERED_PATTERNS.managerClasses.entries()] + .find(([name, info]) => info === m)?.[0] || ''; + return className.toLowerCase().includes('workspace'); + }); + + const storageManagers = managers.filter(m => { + const className = [...DISCOVERED_PATTERNS.managerClasses.entries()] + .find(([name, info]) => info === m)?.[0] || ''; + return className.toLowerCase().includes('storage') || m.filePath.includes('/storage/'); + }); + + const otherManagers = managers.filter(m => !workspaceManagers.includes(m) && !storageManagers.includes(m)); + + console.log(`šŸŽÆ Manager Categories:`); + console.log(` Workspace managers: ${workspaceManagers.length}`); + console.log(` Storage managers: ${storageManagers.length}`); + console.log(` Other managers: ${otherManagers.length}`); + + // Validate each category separately + validateManagerCategory(workspaceManagers, 'Workspace', initPatterns, cleanupPatterns); + validateManagerCategory(storageManagers, 'Storage', initPatterns, cleanupPatterns); + validateManagerCategory(otherManagers, 'Other', initPatterns, cleanupPatterns); +} + +/** + * Validate a specific category of managers + */ +function validateManagerCategory(managers, categoryName, allInitPatterns, allCleanupPatterns) { + if (managers.length === 0) return; + + console.log(`\nšŸ” Validating ${categoryName} managers...`); + + // Find patterns within this category + const categoryInitPatterns = new Map(); + const categoryCleanupPatterns = new Map(); + + managers.forEach(manager => { + manager.initMethods.forEach(method => { + const key = `${method.name}:${method.isAsync}`; + categoryInitPatterns.set(key, (categoryInitPatterns.get(key) || 0) + 1); + }); + + manager.cleanupMethods.forEach(method => { + const key = `${method.name}:${method.isAsync}`; + categoryCleanupPatterns.set(key, (categoryCleanupPatterns.get(key) || 0) + 1); + }); + }); + + const primaryInitPattern = [...categoryInitPatterns.entries()].sort((a, b) => b[1] - a[1])[0]; + const primaryCleanupPattern = [...categoryCleanupPatterns.entries()].sort((a, b) => b[1] - a[1])[0]; + + console.log(` Primary init pattern: ${primaryInitPattern ? primaryInitPattern[0] : 'none'}`); + console.log(` Primary cleanup pattern: ${primaryCleanupPattern ? primaryCleanupPattern[0] : 'none'}`); + + managers.forEach(manager => { + const className = [...DISCOVERED_PATTERNS.managerClasses.entries()] + .find(([name, info]) => info === manager)?.[0] || 'Unknown'; + + // Only require lifecycle methods if other managers in this category have them + if (primaryInitPattern && manager.initMethods.length === 0) { + ERRORS.push({ + file: manager.filePath, + line: manager.lineNumber, + type: 'MANAGER_MISSING_INIT', + message: `${categoryName} manager "${className}" missing initialization method`, + suggestion: `Add ${primaryInitPattern[0].split(':')[0]}() method (pattern used by ${primaryInitPattern[1]} other ${categoryName.toLowerCase()} managers)`, + }); + } + + if (primaryCleanupPattern && manager.cleanupMethods.length === 0) { + // Only flag as error if this category consistently uses cleanup methods + if (primaryCleanupPattern[1] > 1 || managers.length === 1) { + ERRORS.push({ + file: manager.filePath, + line: manager.lineNumber, + type: 'MANAGER_MISSING_CLEANUP', + message: `${categoryName} manager "${className}" missing cleanup method`, + suggestion: `Add ${primaryCleanupPattern[0].split(':')[0]}() method (pattern used by ${primaryCleanupPattern[1]} other ${categoryName.toLowerCase()} managers)`, + }); + } else { + WARNINGS.push({ + file: manager.filePath, + line: manager.lineNumber, + type: 'MANAGER_INCONSISTENT_CLEANUP', + message: `${categoryName} manager "${className}" missing cleanup method`, + suggestion: `Consider adding ${primaryCleanupPattern[0].split(':')[0]}() method for consistency`, + }); + } + } + + // Check for inconsistent patterns within the category + manager.cleanupMethods.forEach(method => { + const pattern = `${method.name}:${method.isAsync}`; + if (primaryCleanupPattern && pattern !== primaryCleanupPattern[0] && categoryCleanupPatterns.size > 1) { + WARNINGS.push({ + file: manager.filePath, + line: manager.lineNumber, + type: 'MANAGER_INCONSISTENT_CLEANUP', + message: `${categoryName} manager "${className}" uses ${pattern} but category standard is ${primaryCleanupPattern[0]}`, + suggestion: `Consider standardizing on ${primaryCleanupPattern[0]} within ${categoryName.toLowerCase()} managers`, + }); + } + }); + + // General manager validations + if (!manager.isExported) { + WARNINGS.push({ + file: manager.filePath, + line: manager.lineNumber, + type: 'MANAGER_NOT_EXPORTED', + message: `Manager class "${className}" should be exported`, + suggestion: 'Export manager classes for use by other packages', + }); + } + + // Only warn about DI for non-utility managers + if (manager.constructorParams === 0 && !className.toLowerCase().includes('factory')) { + WARNINGS.push({ + file: manager.filePath, + line: manager.lineNumber, + type: 'MANAGER_NO_DI', + message: `Manager class "${className}" has no constructor dependencies`, + suggestion: 'Consider dependency injection for better testability', + }); + } + }); +} + +/** + * Validate service class consistency + */ +function validateServiceConsistency(services) { + services.forEach(service => { + const className = [...DISCOVERED_PATTERNS.serviceClasses.entries()] + .find(([name, info]) => info === service)?.[0] || 'Unknown'; + + // Services should generally be stateless + if (service.constructorParams > 5) { + WARNINGS.push({ + file: service.filePath, + line: service.lineNumber, + type: 'SERVICE_TOO_MANY_DEPS', + message: `Service "${className}" has many constructor dependencies (${service.constructorParams})`, + suggestion: 'Consider breaking into smaller services or using composition', + }); + } + }); +} + +/** + * Validate provider class consistency + */ +function validateProviderConsistency(providers) { + providers.forEach(provider => { + const className = [...DISCOVERED_PATTERNS.providerClasses.entries()] + .find(([name, info]) => info === provider)?.[0] || 'Unknown'; + + // Providers should have lifecycle methods + if (provider.initMethods.length === 0 && provider.cleanupMethods.length === 0) { + WARNINGS.push({ + file: provider.filePath, + line: provider.lineNumber, + type: 'PROVIDER_NO_LIFECYCLE', + message: `Provider "${className}" has no lifecycle management`, + suggestion: 'Consider adding initialization and cleanup methods', + }); + } + + // Check for base class extension + if (!provider.extendsClass?.includes('Base') && className !== 'BaseStorageProvider') { + WARNINGS.push({ + file: provider.filePath, + line: provider.lineNumber, + type: 'PROVIDER_NO_BASE', + message: `Provider "${className}" should extend a base provider class`, + suggestion: 'Extend base provider for consistent interface', + }); + } + }); +} + +/** + * Find TypeScript files in all packages + */ +function findArchitectureFiles() { + const files = []; + + function findFilesRecursive(dir, predicate) { + if (!fs.existsSync(dir)) return; + + const entries = fs.readdirSync(dir); + + for (const entry of entries) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + if (!['node_modules', 'build', 'dist', '.next', '.next-build', 'coverage', '__tests__'].includes(entry)) { + findFilesRecursive(fullPath, predicate); + } + } else if (predicate(fullPath)) { + files.push(fullPath); + } + } + } + + // Find TypeScript files in all packages + const packagesDir = path.join(process.cwd(), 'packages'); + if (fs.existsSync(packagesDir)) { + findFilesRecursive(packagesDir, (file) => + (file.endsWith('.ts') || file.endsWith('.tsx')) && + !file.endsWith('.d.ts') && + !file.includes('.test.') && + !file.includes('.spec.') + ); + } + + return files; +} + +/** + * Discovery phase - analyze all files to understand current patterns + */ +function runDiscoveryPhase(filePaths) { + console.log('šŸ” Phase 1: Discovering architectural patterns...'); + + const program = createProgram(filePaths); + + filePaths.forEach(filePath => { + const sourceFile = program.getSourceFile(filePath); + if (sourceFile) { + discoverPatterns(filePath, sourceFile); + } + }); +} + +/** + * Validation phase - check consistency based on discovered patterns + */ +function runValidationPhase() { + console.log('\nšŸ” Phase 2: Validating pattern consistency...'); + validatePatternConsistency(); +} + +/** + * Main validation function + */ +function validateArchitecturePatterns() { + console.log('šŸš€ Dynamic Architecture Patterns Validation'); + console.log(' Discovering and validating actual codebase patterns...\n'); + + const files = findArchitectureFiles(); + console.log(` Found ${files.length} TypeScript files to analyze\n`); + + // Phase 1: Discovery + runDiscoveryPhase(files); + + // Phase 2: Validation + runValidationPhase(); + + // Report results + let hasErrors = false; + + if (ERRORS.length > 0) { + console.log(`\nāŒ Found ${ERRORS.length} consistency errors:\n`); + ERRORS.forEach((error) => { + console.log(`šŸ“ ${error.file}:${error.line} [${error.type}]`); + console.log(` ${error.message}`); + console.log(` šŸ’” ${error.suggestion}\n`); + }); + hasErrors = true; + } + + if (WARNINGS.length > 0) { + console.log(`\nāš ļø Found ${WARNINGS.length} consistency warnings:\n`); + WARNINGS.forEach((warning) => { + console.log(`šŸ“ ${warning.file}:${warning.line} [${warning.type}]`); + console.log(` ${warning.message}`); + console.log(` šŸ’” ${warning.suggestion}\n`); + }); + } + + if (!hasErrors && WARNINGS.length === 0) { + console.log('āœ… All architectural patterns are consistent!'); + } else if (!hasErrors) { + console.log('āœ… No critical consistency errors found!'); + console.log('šŸ’” Consider addressing warnings to improve architecture consistency.'); + } + + // Summary of discovered patterns + console.log('\nšŸ“Š Pattern Discovery Summary:'); + console.log(` Manager classes: ${DISCOVERED_PATTERNS.managerClasses.size}`); + console.log(` Service classes: ${DISCOVERED_PATTERNS.serviceClasses.size}`); + console.log(` Provider classes: ${DISCOVERED_PATTERNS.providerClasses.size}`); + console.log(` Initialization methods: ${Array.from(DISCOVERED_PATTERNS.initializationMethods).join(', ')}`); + console.log(` Cleanup methods: ${Array.from(DISCOVERED_PATTERNS.lifecycleMethods).join(', ')}`); + + process.exit(hasErrors ? 1 : 0); +} + +// Run validation if called directly +if (require.main === module) { + validateArchitecturePatterns(); +} + +module.exports = { validateArchitecturePatterns }; \ No newline at end of file diff --git a/scripts/validate-security-performance-ast.js b/scripts/validate-security-performance-ast.js new file mode 100755 index 00000000..daf3c862 --- /dev/null +++ b/scripts/validate-security-performance-ast.js @@ -0,0 +1,637 @@ +#!/usr/bin/env node + +/** + * Security and Performance Validation (AST-based) + * Uses TypeScript compiler API to detect security issues and performance anti-patterns + */ + +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); + +const ERRORS = []; +const WARNINGS = []; + +/** + * Create TypeScript program for AST analysis + */ +function createProgram(filePaths) { + const compilerOptions = { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + allowJs: true, + jsx: ts.JsxEmit.ReactJSX, + strict: false, + skipLibCheck: true, + skipDefaultLibCheck: true, + noEmit: true, + allowSyntheticDefaultImports: true, + esModuleInterop: true, + }; + + const host = ts.createCompilerHost(compilerOptions); + return ts.createProgram(filePaths, compilerOptions, host); +} + +/** + * Visit AST nodes recursively with proper error handling + */ +function visitNode(node, sourceFile, visitor) { + try { + visitor(node, sourceFile); + node.forEachChild(child => visitNode(child, sourceFile, visitor)); + } catch (error) { + // Skip problematic nodes and continue + console.warn(`āš ļø Skipping problematic node in ${sourceFile.fileName}: ${error.message}`); + } +} + +/** + * Get line number from AST node with error handling + */ +function getLineNumber(sourceFile, node) { + try { + if (!sourceFile || !node) return 1; + const lineAndChar = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); + return lineAndChar.line + 1; + } catch (error) { + return 1; // Fallback to line 1 if position cannot be determined + } +} + +/** + * Get node text with error handling + */ +function getNodeText(sourceFile, node) { + try { + if (!sourceFile || !node) return ''; + return node.getText(sourceFile); + } catch (error) { + return ''; // Fallback to empty string + } +} + +/** + * Get package name from file path + */ +function getPackageName(filePath) { + const match = filePath.match(/packages\/([^\/]+)\//); + return match ? match[1] : null; +} + +/** + * Validate security patterns + */ +function validateSecurity(filePath, sourceFile) { + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + const nodeText = getNodeText(sourceFile, node); + + // Check for potential XSS vulnerabilities (innerHTML usage) + if (ts.isPropertyAccessExpression(node) && + node.name.text === 'innerHTML') { + ERRORS.push({ + file: filePath, + line: lineNum, + type: 'SECURITY_XSS_INNERHTML', + message: 'Potential XSS vulnerability - avoid innerHTML with user data', + suggestion: 'Use textContent, createElement, or sanitize input before innerHTML', + }); + } + + // Check for eval() usage + if (ts.isCallExpression(node) && + node.expression && ts.isIdentifier(node.expression) && + node.expression.text === 'eval') { + ERRORS.push({ + file: filePath, + line: lineNum, + type: 'SECURITY_EVAL', + message: 'eval() usage is a security risk', + suggestion: 'Use JSON.parse() for data or refactor to avoid eval()', + }); + } + + // Check for Function constructor + if (ts.isNewExpression(node) && + node.expression && ts.isIdentifier(node.expression) && + node.expression.text === 'Function') { + ERRORS.push({ + file: filePath, + line: lineNum, + type: 'SECURITY_FUNCTION_CONSTRUCTOR', + message: 'Function constructor can be a security risk', + suggestion: 'Use regular function declarations or arrow functions', + }); + } + + // Check for dangerous regex patterns (ReDoS) + if (ts.isRegularExpressionLiteral(node)) { + const regexText = node.text; + + // Check for nested quantifiers (potential ReDoS) + if (regexText.includes('(.*)*') || regexText.includes('(.*)+') || regexText.includes('(.+)*')) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'SECURITY_REGEX_REDOS', + message: 'Regex pattern may be vulnerable to ReDoS attacks', + suggestion: 'Avoid nested quantifiers and use more specific patterns', + }); + } + + // Check for overly broad patterns + if (regexText.includes('.*') && regexText.length < 10) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'SECURITY_REGEX_BROAD', + message: 'Overly broad regex pattern', + suggestion: 'Use more specific patterns to avoid performance issues', + }); + } + } + + // Check for hardcoded secrets (basic patterns) + if (ts.isStringLiteral(node)) { + const stringValue = node.text; + + // Check for potential API keys or tokens + if (/^[A-Za-z0-9]{32,}$/.test(stringValue) && stringValue.length > 20) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'SECURITY_HARDCODED_SECRET', + message: 'Potential hardcoded secret or API key detected', + suggestion: 'Use environment variables or secure configuration for secrets', + }); + } + + // Check for common secret patterns + const secretPatterns = [ + /password\s*[:=]\s*['"][^'"]{8,}['"]/i, + /api[_-]?key\s*[:=]\s*['"][^'"]{16,}['"]/i, + /secret\s*[:=]\s*['"][^'"]{16,}['"]/i, + /token\s*[:=]\s*['"][^'"]{16,}['"]/i, + ]; + + for (const pattern of secretPatterns) { + if (pattern.test(nodeText)) { + ERRORS.push({ + file: filePath, + line: lineNum, + type: 'SECURITY_HARDCODED_CREDENTIALS', + message: 'Hardcoded credentials detected', + suggestion: 'Use environment variables or secure configuration', + }); + break; + } + } + } + + // Check for SQL injection vulnerabilities (string concatenation in queries) + if (ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.PlusToken) { + const leftText = getNodeText(sourceFile, node.left); + const rightText = getNodeText(sourceFile, node.right); + + if ((leftText.includes('SELECT') || leftText.includes('INSERT') || + leftText.includes('UPDATE') || leftText.includes('DELETE')) && + (rightText.includes('input') || rightText.includes('param') || rightText.includes('user'))) { + ERRORS.push({ + file: filePath, + line: lineNum, + type: 'SECURITY_SQL_INJECTION', + message: 'Potential SQL injection - avoid string concatenation in queries', + suggestion: 'Use parameterized queries or prepared statements', + }); + } + } + + // Check for path traversal vulnerabilities + if (ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + node.expression.name.text === 'join' && + node.arguments.length > 1) { + + const hasUserInput = node.arguments.some(arg => { + const argText = getNodeText(sourceFile, arg); + return argText.includes('input') || argText.includes('param') || + argText.includes('query') || argText.includes('req.'); + }); + + if (hasUserInput) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'SECURITY_PATH_TRAVERSAL', + message: 'Potential path traversal - validate user input in path operations', + suggestion: 'Sanitize and validate file paths from user input', + }); + } + } + }); +} + +/** + * Validate performance patterns + */ +function validatePerformance(filePath, sourceFile) { + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + const nodeText = getNodeText(sourceFile, node); + + // Check for synchronous blocking operations + if (ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + node.expression.name.text.endsWith('Sync')) { + const methodName = node.expression.name.text; + const blockingMethods = ['readFileSync', 'writeFileSync', 'mkdirSync', 'rmSync', 'statSync']; + + if (blockingMethods.includes(methodName) && !filePath.includes('.test.')) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'PERFORMANCE_SYNC_OPERATION', + message: `Synchronous ${methodName} can block event loop`, + suggestion: `Use async version: ${methodName.replace('Sync', '')}`, + }); + } + } + + // Check for inefficient loops + if (ts.isForInStatement(node)) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'PERFORMANCE_FOR_IN_LOOP', + message: 'for...in loop can be slow for arrays', + suggestion: 'Use for...of, forEach, or traditional for loop for arrays', + }); + } + + // Check for nested loops + if (ts.isForStatement(node) || ts.isForOfStatement(node) || ts.isWhileStatement(node)) { + let hasNestedLoop = false; + + visitNode(node, sourceFile, (childNode) => { + if (childNode !== node && + (ts.isForStatement(childNode) || ts.isForOfStatement(childNode) || + ts.isWhileStatement(childNode))) { + hasNestedLoop = true; + } + }); + + if (hasNestedLoop) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'PERFORMANCE_NESTED_LOOPS', + message: 'Nested loops can cause performance issues', + suggestion: 'Consider algorithm optimization or using Map/Set for lookups', + }); + } + } + + // Check for excessive object property access in loops + if ((ts.isForStatement(node) || ts.isForOfStatement(node) || ts.isWhileStatement(node)) && + node.statement && ts.isBlock(node.statement)) { + + let hasRepeatedPropertyAccess = false; + const propertyAccesses = new Map(); + + visitNode(node.statement, sourceFile, (childNode) => { + if (ts.isPropertyAccessExpression(childNode)) { + const accessText = getNodeText(sourceFile, childNode); + const count = propertyAccesses.get(accessText) || 0; + propertyAccesses.set(accessText, count + 1); + + if (count > 2) { + hasRepeatedPropertyAccess = true; + } + } + }); + + if (hasRepeatedPropertyAccess) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'PERFORMANCE_REPEATED_PROPERTY_ACCESS', + message: 'Repeated property access in loop', + suggestion: 'Cache property values outside the loop', + }); + } + } + + // Check for inefficient array operations + if (ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression)) { + const methodName = node.expression.name.text; + + // Check for inefficient search patterns + if (methodName === 'find' && node.parent && ts.isPropertyAccessExpression(node.parent)) { + const chainedMethod = node.parent.name.text; + if (['length', 'indexOf'].includes(chainedMethod)) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'PERFORMANCE_INEFFICIENT_SEARCH', + message: 'Inefficient search pattern detected', + suggestion: 'Use includes(), some(), or Set for existence checks', + }); + } + } + + // Check for Array.push in loops + if (methodName === 'push') { + const parent = node.parent; + while (parent) { + if (ts.isForStatement(parent) || ts.isForOfStatement(parent) || ts.isWhileStatement(parent)) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'PERFORMANCE_ARRAY_PUSH_LOOP', + message: 'Array.push in loop can be inefficient for large datasets', + suggestion: 'Consider pre-allocating array size or using different data structure', + }); + break; + } + parent = parent.parent; + } + } + } + + // Check for memory leaks - missing cleanup + if (ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + ['setInterval', 'setTimeout'].includes(node.expression.text)) { + + // Look for corresponding clear calls in the same function or class + let hasCleanup = false; + const functionNode = node.parent; + + if (functionNode) { + visitNode(functionNode, sourceFile, (childNode) => { + if (ts.isCallExpression(childNode) && + ts.isIdentifier(childNode.expression) && + ['clearInterval', 'clearTimeout'].includes(childNode.expression.text)) { + hasCleanup = true; + } + }); + } + + if (!hasCleanup) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'PERFORMANCE_TIMER_LEAK', + message: 'Timer without cleanup can cause memory leaks', + suggestion: 'Store timer ID and clear it in cleanup/dispose method', + }); + } + } + + // Check for large object literals (potential memory issues) + if (ts.isObjectLiteralExpression(node)) { + const propertyCount = node.properties.length; + if (propertyCount > 50) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'PERFORMANCE_LARGE_OBJECT', + message: `Large object literal with ${propertyCount} properties`, + suggestion: 'Consider breaking into smaller objects or using Map for dynamic data', + }); + } + } + + // Check for recursive functions without memoization + if ((ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) && node.name) { + const functionName = node.name.text; + let hasRecursiveCall = false; + let hasMemoization = false; + + if (node.body) { + visitNode(node.body, sourceFile, (childNode) => { + if (ts.isCallExpression(childNode) && + ts.isIdentifier(childNode.expression) && + childNode.expression.text === functionName) { + hasRecursiveCall = true; + } + + // Check for memoization patterns (cache, memo, etc.) + const childText = getNodeText(sourceFile, childNode); + if (childText.includes('cache') || childText.includes('memo') || + childText.includes('Map') || childText.includes('WeakMap')) { + hasMemoization = true; + } + }); + } + + if (hasRecursiveCall && !hasMemoization && functionName !== 'visitNode') { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'PERFORMANCE_RECURSIVE_NO_MEMO', + message: `Recursive function "${functionName}" without memoization`, + suggestion: 'Consider adding memoization for better performance', + }); + } + } + }); +} + +/** + * Validate resource management patterns + */ +function validateResourceManagement(filePath, sourceFile) { + let hasResourceCreation = false; + let hasResourceCleanup = false; + + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + const nodeText = getNodeText(sourceFile, node); + + // Check for resource creation patterns + if (ts.isNewExpression(node) && node.expression && ts.isIdentifier(node.expression)) { + const className = node.expression.text; + const resourceClasses = ['EventEmitter', 'Server', 'Database', 'Connection', 'Stream']; + + if (resourceClasses.some(rc => className.includes(rc))) { + hasResourceCreation = true; + } + } + + // Check for file handle creation + if (ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + ['createReadStream', 'createWriteStream', 'open'].includes(node.expression.name.text)) { + hasResourceCreation = true; + } + + // Check for cleanup methods + if (ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression)) { + const methodName = node.expression.name.text; + const cleanupMethods = ['close', 'end', 'destroy', 'dispose', 'cleanup', 'disconnect']; + + if (cleanupMethods.includes(methodName)) { + hasResourceCleanup = true; + } + } + + // Check for event listener cleanup + if (ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + node.expression.name.text === 'addEventListener') { + + // Look for corresponding removeEventListener + let hasRemoveListener = false; + const parentFunction = node.parent; + + if (parentFunction) { + visitNode(parentFunction, sourceFile, (childNode) => { + if (ts.isCallExpression(childNode) && + ts.isPropertyAccessExpression(childNode.expression) && + childNode.expression.name.text === 'removeEventListener') { + hasRemoveListener = true; + } + }); + } + + if (!hasRemoveListener) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'PERFORMANCE_EVENT_LISTENER_LEAK', + message: 'Event listener without removal can cause memory leaks', + suggestion: 'Add removeEventListener in cleanup/dispose method', + }); + } + } + }); + + // File-level validation for resource management + if (hasResourceCreation && !hasResourceCleanup) { + WARNINGS.push({ + file: filePath, + line: 1, + type: 'PERFORMANCE_RESOURCE_NO_CLEANUP', + message: 'File creates resources but has no cleanup patterns', + suggestion: 'Implement dispose/cleanup methods for proper resource management', + }); + } +} + +/** + * Validate files using AST + */ +function validateFilesWithAST(filePaths) { + console.log(`šŸ” Creating TypeScript program for ${filePaths.length} files...`); + + const program = createProgram(filePaths); + + filePaths.forEach(filePath => { + const sourceFile = program.getSourceFile(filePath); + if (!sourceFile) { + console.warn(`āš ļø Could not parse ${filePath}`); + return; + } + + // Run all validations + validateSecurity(filePath, sourceFile); + validatePerformance(filePath, sourceFile); + validateResourceManagement(filePath, sourceFile); + }); +} + +/** + * Find TypeScript files in all packages + */ +function findSecurityPerformanceFiles() { + const files = []; + + function findFilesRecursive(dir, predicate) { + if (!fs.existsSync(dir)) return; + + const entries = fs.readdirSync(dir); + + for (const entry of entries) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + if (!['node_modules', 'build', 'dist', '.next', '.next-build', 'coverage', '__tests__'].includes(entry)) { + findFilesRecursive(fullPath, predicate); + } + } else if (predicate(fullPath)) { + files.push(fullPath); + } + } + } + + // Find TypeScript files in all packages + const packagesDir = path.join(process.cwd(), 'packages'); + if (fs.existsSync(packagesDir)) { + findFilesRecursive(packagesDir, (file) => + (file.endsWith('.ts') || file.endsWith('.tsx')) && + !file.endsWith('.d.ts') && + !file.includes('.test.') && + !file.includes('.spec.') + ); + } + + return files; +} + +/** + * Main validation function + */ +function validateSecurityAndPerformance() { + console.log('šŸ” Validating security and performance patterns (AST-based)...'); + + const files = findSecurityPerformanceFiles(); + console.log(` Found ${files.length} TypeScript files to analyze`); + + validateFilesWithAST(files); + + // Report results + let hasErrors = false; + + if (ERRORS.length > 0) { + console.log(`\nāŒ Found ${ERRORS.length} security and performance errors:\n`); + ERRORS.forEach((error) => { + console.log(`šŸ“ ${error.file}:${error.line} [${error.type}]`); + console.log(` ${error.message}`); + console.log(` šŸ’” ${error.suggestion}\n`); + }); + hasErrors = true; + } + + if (WARNINGS.length > 0) { + console.log(`\nāš ļø Found ${WARNINGS.length} security and performance warnings:\n`); + WARNINGS.forEach((warning) => { + console.log(`šŸ“ ${warning.file}:${warning.line} [${warning.type}]`); + console.log(` ${warning.message}`); + console.log(` šŸ’” ${warning.suggestion}\n`); + }); + } + + if (!hasErrors && WARNINGS.length === 0) { + console.log('āœ… No security or performance issues detected!'); + } else if (!hasErrors) { + console.log('āœ… No critical security or performance errors found!'); + console.log('šŸ’” Consider addressing warnings to improve security and performance.'); + } + + // Only exit with error code for actual errors, not warnings + process.exit(hasErrors ? 1 : 0); +} + +// Run validation if called directly +if (require.main === module) { + validateSecurityAndPerformance(); +} + +module.exports = { validateSecurityAndPerformance }; \ No newline at end of file diff --git a/scripts/validate-testing-standards-ast.js b/scripts/validate-testing-standards-ast.js new file mode 100755 index 00000000..dca483cd --- /dev/null +++ b/scripts/validate-testing-standards-ast.js @@ -0,0 +1,646 @@ +#!/usr/bin/env node + +/** + * Testing Standards Validation (AST-based) + * Uses TypeScript compiler API to enforce testing patterns and standards + */ + +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); + +const ERRORS = []; +const WARNINGS = []; + +/** + * Create TypeScript program for AST analysis + */ +function createProgram(filePaths) { + const compilerOptions = { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + allowJs: true, + jsx: ts.JsxEmit.ReactJSX, + strict: false, + skipLibCheck: true, + skipDefaultLibCheck: true, + noEmit: true, + allowSyntheticDefaultImports: true, + esModuleInterop: true, + }; + + const host = ts.createCompilerHost(compilerOptions); + return ts.createProgram(filePaths, compilerOptions, host); +} + +/** + * Visit AST nodes recursively with proper error handling + */ +function visitNode(node, sourceFile, visitor) { + try { + visitor(node, sourceFile); + node.forEachChild(child => visitNode(child, sourceFile, visitor)); + } catch (error) { + // Skip problematic nodes and continue + console.warn(`āš ļø Skipping problematic node in ${sourceFile.fileName}: ${error.message}`); + } +} + +/** + * Get line number from AST node with error handling + */ +function getLineNumber(sourceFile, node) { + try { + if (!sourceFile || !node) return 1; + const lineAndChar = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); + return lineAndChar.line + 1; + } catch (error) { + return 1; // Fallback to line 1 if position cannot be determined + } +} + +/** + * Get node text with error handling + */ +function getNodeText(sourceFile, node) { + try { + if (!sourceFile || !node) return ''; + return node.getText(sourceFile); + } catch (error) { + return ''; // Fallback to empty string + } +} + +/** + * Check if file is a test file + */ +function isTestFile(filePath) { + return filePath.includes('.test.') || filePath.includes('.spec.') || filePath.includes('/__tests__/'); +} + +/** + * Get package name from file path + */ +function getPackageName(filePath) { + const match = filePath.match(/packages\/([^\/]+)\//); + return match ? match[1] : null; +} + +/** + * Validate test file structure and organization + */ +function validateTestStructure(filePath, sourceFile) { + let hasDescribeBlocks = false; + let hasItBlocks = false; + let hasBeforeEach = false; + let hasAfterEach = false; + let hasSetupTeardown = false; + let hasVitest = false; + let hasProperImports = false; + + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + + // Check imports for testing frameworks + if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) { + const importPath = node.moduleSpecifier.text; + if (importPath === 'vitest' || importPath.includes('vitest')) { + hasVitest = true; + hasProperImports = true; + } + } + + // Check for test structure function calls + if (ts.isCallExpression(node) && node.expression && ts.isIdentifier(node.expression)) { + const functionName = node.expression.text; + + switch (functionName) { + case 'describe': + hasDescribeBlocks = true; + + // Check describe block naming + if (node.arguments.length > 0 && ts.isStringLiteral(node.arguments[0])) { + const describeName = node.arguments[0].text; + if (!describeName || describeName.length < 3) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'TEST_DESCRIBE_NAMING', + message: 'Describe block should have meaningful name', + suggestion: 'Use descriptive names like "ComponentName" or "methodName"', + }); + } + } + break; + + case 'it': + case 'test': + hasItBlocks = true; + + // Check test naming + if (node.arguments.length > 0 && ts.isStringLiteral(node.arguments[0])) { + const testName = node.arguments[0].text; + if (!testName.startsWith('should')) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'TEST_NAMING', + message: 'Test name should start with "should"', + suggestion: 'Use pattern: "should describe expected behavior"', + }); + } + + if (testName.length < 10) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'TEST_NAME_TOO_SHORT', + message: 'Test name too short - should be descriptive', + suggestion: 'Clearly describe what behavior is being tested', + }); + } + } + break; + + case 'beforeEach': + hasBeforeEach = true; + hasSetupTeardown = true; + break; + + case 'afterEach': + hasAfterEach = true; + hasSetupTeardown = true; + break; + + case 'beforeAll': + case 'afterAll': + hasSetupTeardown = true; + break; + } + } + }); + + // File-level validations + if (!hasProperImports) { + ERRORS.push({ + file: filePath, + line: 1, + type: 'TEST_MISSING_FRAMEWORK', + message: 'Test file missing testing framework imports', + suggestion: 'Import vitest functions: import { describe, it, expect, beforeEach, afterEach } from "vitest"', + }); + } + + if (!hasDescribeBlocks && hasItBlocks) { + WARNINGS.push({ + file: filePath, + line: 1, + type: 'TEST_NO_DESCRIBE', + message: 'Test file should organize tests in describe blocks', + suggestion: 'Group related tests using describe blocks for better organization', + }); + } + + if (!hasItBlocks) { + ERRORS.push({ + file: filePath, + line: 1, + type: 'TEST_NO_TESTS', + message: 'Test file contains no test cases', + suggestion: 'Add test cases using it() or test() functions', + }); + } +} + +/** + * Validate test isolation patterns + */ +function validateTestIsolation(filePath, sourceFile) { + let hasFileSystemOperations = false; + let hasProcessCwdUsage = false; + let hasEnvVarChanges = false; + let hasProperCleanup = false; + let hasTemporaryDirectories = false; + + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + const nodeText = getNodeText(sourceFile, node); + + // Check for file system operations + if (ts.isCallExpression(node) && node.expression && ts.isPropertyAccessExpression(node.expression)) { + const objectName = getNodeText(sourceFile, node.expression.expression); + const methodName = node.expression.name.text; + + if (objectName === 'fs' && ['writeFileSync', 'mkdirSync', 'rmSync', 'mkdtemp'].includes(methodName)) { + hasFileSystemOperations = true; + + if (methodName === 'mkdtemp') { + hasTemporaryDirectories = true; + } + } + } + + // Check for process.cwd() usage + if (ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === 'process' && + node.expression.name.text === 'cwd') { + hasProcessCwdUsage = true; + } + + // Check for environment variable changes + if (ts.isBinaryExpression(node) && + ts.isPropertyAccessExpression(node.left) && + getNodeText(sourceFile, node.left).includes('process.env')) { + hasEnvVarChanges = true; + } + + // Check for proper cleanup in afterEach + if (ts.isCallExpression(node) && + node.expression && ts.isIdentifier(node.expression) && + node.expression.text === 'afterEach') { + hasProperCleanup = true; + } + }); + + // File-level validations for isolation + if (hasFileSystemOperations && !hasTemporaryDirectories) { + WARNINGS.push({ + file: filePath, + line: 1, + type: 'TEST_FILE_OPERATIONS_NO_TEMP', + message: 'File system operations should use temporary directories', + suggestion: 'Use fs.mkdtemp() to create isolated test directories', + }); + } + + if ((hasFileSystemOperations || hasProcessCwdUsage || hasEnvVarChanges) && !hasProperCleanup) { + ERRORS.push({ + file: filePath, + line: 1, + type: 'TEST_MISSING_CLEANUP', + message: 'Tests with side effects missing cleanup in afterEach', + suggestion: 'Add afterEach block to restore original state (cwd, env, files)', + }); + } + + if (hasProcessCwdUsage) { + WARNINGS.push({ + file: filePath, + line: 1, + type: 'TEST_PROCESS_CWD', + message: 'Tests changing process.cwd() should store and restore original', + suggestion: 'Store originalCwd = process.cwd() and restore in afterEach', + }); + } + + if (hasEnvVarChanges) { + WARNINGS.push({ + file: filePath, + line: 1, + type: 'TEST_ENV_CHANGES', + message: 'Tests modifying environment variables should restore them', + suggestion: 'Store original process.env and restore in afterEach', + }); + } +} + +/** + * Validate async test patterns + */ +function validateAsyncTestPatterns(filePath, sourceFile) { + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + + // Check test function definitions for async patterns + if (ts.isCallExpression(node) && + node.expression && ts.isIdentifier(node.expression) && + ['it', 'test', 'beforeEach', 'afterEach'].includes(node.expression.text)) { + + // Get the test function (second argument) + if (node.arguments.length > 1) { + const testFunction = node.arguments[1]; + + if (ts.isArrowFunction(testFunction) || ts.isFunctionExpression(testFunction)) { + const isAsync = testFunction.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword); + const functionBody = getNodeText(sourceFile, testFunction); + + // Check for await usage without async + if (functionBody.includes('await') && !isAsync) { + ERRORS.push({ + file: filePath, + line: lineNum, + type: 'TEST_AWAIT_WITHOUT_ASYNC', + message: 'Test function uses await but is not async', + suggestion: 'Mark test function as async when using await', + }); + } + + // Check for Promise-returning methods without await + if (functionBody.includes('.initialize(') || + functionBody.includes('.dispose(') || + functionBody.includes('.save(') || + functionBody.includes('.create(')) { + if (!functionBody.includes('await') && isAsync) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'TEST_MISSING_AWAIT', + message: 'Async test may be missing await on Promise-returning calls', + suggestion: 'Ensure all async operations are properly awaited', + }); + } + } + + // Check for proper timeout handling + if (isAsync && !functionBody.includes('timeout') && functionBody.length > 500) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'TEST_LONG_ASYNC_NO_TIMEOUT', + message: 'Long async test should consider timeout configuration', + suggestion: 'Add timeout configuration for long-running async tests', + }); + } + } + } + } + }); +} + +/** + * Validate test assertions and expectations + */ +function validateTestAssertions(filePath, sourceFile) { + let hasExpectCalls = false; + let hasProperMatchers = false; + + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + + // Check for expect() calls + if (ts.isCallExpression(node) && + node.expression && ts.isIdentifier(node.expression) && + node.expression.text === 'expect') { + hasExpectCalls = true; + + // Check if expect is chained with proper matchers + const parent = node.parent; + if (ts.isPropertyAccessExpression(parent)) { + const matcher = parent.name.text; + const validMatchers = [ + 'toBe', 'toEqual', 'toBeNull', 'toBeUndefined', 'toBeDefined', + 'toBeTruthy', 'toBeFalsy', 'toContain', 'toHaveLength', + 'toThrow', 'toHaveBeenCalled', 'toHaveBeenCalledWith', + 'toBeInstanceOf', 'toMatchObject', 'toBeGreaterThan', 'toBeLessThan' + ]; + + if (validMatchers.includes(matcher)) { + hasProperMatchers = true; + } else { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'TEST_UNKNOWN_MATCHER', + message: `Unknown or custom matcher: ${matcher}`, + suggestion: 'Use standard Jest/Vitest matchers for better compatibility', + }); + } + } + } + + // Check for async expect patterns + if (ts.isAwaitExpression(node) && + ts.isCallExpression(node.expression) && + node.expression.expression && ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === 'expect') { + + // Look for .rejects or .resolves + const expectCall = getNodeText(sourceFile, node); + if (!expectCall.includes('.rejects') && !expectCall.includes('.resolves')) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'TEST_ASYNC_EXPECT', + message: 'Awaited expect should use .resolves or .rejects', + suggestion: 'Use expect(promise).resolves.toBe() or expect(promise).rejects.toThrow()', + }); + } + } + }); + + // Check for tests without assertions + if (isTestFile(filePath) && !hasExpectCalls) { + WARNINGS.push({ + file: filePath, + line: 1, + type: 'TEST_NO_ASSERTIONS', + message: 'Test file contains no assertions', + suggestion: 'Add expect() calls to verify test behavior', + }); + } +} + +/** + * Validate mock and spy usage + */ +function validateMockPatterns(filePath, sourceFile) { + let hasMocks = false; + let hasSpies = false; + let hasViMocks = false; + + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + + // Check for mock imports + if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) { + const importPath = node.moduleSpecifier.text; + if (importPath === 'vitest' && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) { + node.importClause.namedBindings.elements.forEach(element => { + const importName = element.name.text; + if (['vi', 'mock', 'spyOn'].includes(importName)) { + hasViMocks = true; + } + }); + } + } + + // Check for mock function calls + if (ts.isCallExpression(node) && node.expression) { + const callText = getNodeText(sourceFile, node); + + if (callText.includes('vi.fn()') || callText.includes('vi.mock(')) { + hasMocks = true; + hasViMocks = true; + } + + if (callText.includes('vi.spyOn(') || callText.includes('spyOn(')) { + hasSpies = true; + hasViMocks = true; + } + } + + // Check for global mock usage (should be avoided) + if (ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + node.expression.expression && ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === 'global') { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'TEST_GLOBAL_MOCK', + message: 'Global mocks can cause test isolation issues', + suggestion: 'Use local mocks or vi.mock() for better test isolation', + }); + } + }); + + // Check for mock cleanup + if ((hasMocks || hasSpies) && hasViMocks) { + // Should have cleanup in afterEach + let hasCleanup = false; + + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + if (ts.isCallExpression(node) && + node.expression && ts.isIdentifier(node.expression) && + node.expression.text === 'afterEach') { + const cleanupText = getNodeText(sourceFile, node); + if (cleanupText.includes('vi.clearAllMocks') || cleanupText.includes('vi.restoreAllMocks')) { + hasCleanup = true; + } + } + }); + + if (!hasCleanup) { + WARNINGS.push({ + file: filePath, + line: 1, + type: 'TEST_MOCK_NO_CLEANUP', + message: 'Tests using mocks should clean up in afterEach', + suggestion: 'Add vi.clearAllMocks() or vi.restoreAllMocks() in afterEach', + }); + } + } +} + +/** + * Validate files using AST + */ +function validateFilesWithAST(filePaths) { + console.log(`šŸ” Creating TypeScript program for ${filePaths.length} files...`); + + const program = createProgram(filePaths); + + filePaths.forEach(filePath => { + const sourceFile = program.getSourceFile(filePath); + if (!sourceFile) { + console.warn(`āš ļø Could not parse ${filePath}`); + return; + } + + // Only validate test files + if (isTestFile(filePath)) { + validateTestStructure(filePath, sourceFile); + validateTestIsolation(filePath, sourceFile); + validateAsyncTestPatterns(filePath, sourceFile); + validateTestAssertions(filePath, sourceFile); + validateMockPatterns(filePath, sourceFile); + } + }); +} + +/** + * Find test files in all packages + */ +function findTestFiles() { + const files = []; + + function findFilesRecursive(dir, predicate) { + if (!fs.existsSync(dir)) return; + + const entries = fs.readdirSync(dir); + + for (const entry of entries) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + if (!['node_modules', 'build', 'dist', '.next', '.next-build', 'coverage'].includes(entry)) { + findFilesRecursive(fullPath, predicate); + } + } else if (predicate(fullPath)) { + files.push(fullPath); + } + } + } + + // Find test files in all packages + const packagesDir = path.join(process.cwd(), 'packages'); + if (fs.existsSync(packagesDir)) { + findFilesRecursive(packagesDir, (file) => + (file.endsWith('.ts') || file.endsWith('.tsx')) && + !file.endsWith('.d.ts') && + isTestFile(file) + ); + } + + return files; +} + +/** + * Main validation function + */ +function validateTestingStandards() { + console.log('šŸ” Validating testing standards (AST-based)...'); + + const files = findTestFiles(); + console.log(` Found ${files.length} test files to analyze`); + + if (files.length === 0) { + console.log('āš ļø No test files found - consider adding tests for better code quality'); + return; + } + + validateFilesWithAST(files); + + // Report results + let hasErrors = false; + + if (ERRORS.length > 0) { + console.log(`\nāŒ Found ${ERRORS.length} testing standard errors:\n`); + ERRORS.forEach((error) => { + console.log(`šŸ“ ${error.file}:${error.line} [${error.type}]`); + console.log(` ${error.message}`); + console.log(` šŸ’” ${error.suggestion}\n`); + }); + hasErrors = true; + } + + if (WARNINGS.length > 0) { + console.log(`\nāš ļø Found ${WARNINGS.length} testing standard warnings:\n`); + WARNINGS.forEach((warning) => { + console.log(`šŸ“ ${warning.file}:${warning.line} [${warning.type}]`); + console.log(` ${warning.message}`); + console.log(` šŸ’” ${warning.suggestion}\n`); + }); + } + + if (!hasErrors && WARNINGS.length === 0) { + console.log('āœ… All testing standards are followed!'); + } else if (!hasErrors) { + console.log('āœ… No critical testing standard errors found!'); + console.log('šŸ’” Consider addressing warnings to improve test quality.'); + } + + // Only exit with error code for actual errors, not warnings + process.exit(hasErrors ? 1 : 0); +} + +// Run validation if called directly +if (require.main === module) { + validateTestingStandards(); +} + +module.exports = { validateTestingStandards }; \ No newline at end of file diff --git a/scripts/validate-typescript-best-practices-ast.js b/scripts/validate-typescript-best-practices-ast.js new file mode 100755 index 00000000..1587b62e --- /dev/null +++ b/scripts/validate-typescript-best-practices-ast.js @@ -0,0 +1,496 @@ +#!/usr/bin/env node + +/** + * TypeScript Best Practices Validation (AST-based) + * Uses TypeScript compiler API to enforce TypeScript coding standards + */ + +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); + +const ERRORS = []; +const WARNINGS = []; + +/** + * Create TypeScript program for AST analysis + */ +function createProgram(filePaths) { + const compilerOptions = { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + allowJs: true, + jsx: ts.JsxEmit.ReactJSX, + strict: true, + skipLibCheck: true, + skipDefaultLibCheck: true, + noEmit: true, + allowSyntheticDefaultImports: true, + esModuleInterop: true, + }; + + const host = ts.createCompilerHost(compilerOptions); + return ts.createProgram(filePaths, compilerOptions, host); +} + +/** + * Visit AST nodes recursively with proper error handling + */ +function visitNode(node, sourceFile, visitor) { + try { + visitor(node, sourceFile); + node.forEachChild(child => visitNode(child, sourceFile, visitor)); + } catch (error) { + // Skip problematic nodes and continue + console.warn(`āš ļø Skipping problematic node in ${sourceFile.fileName}: ${error.message}`); + } +} + +/** + * Get line number from AST node with error handling + */ +function getLineNumber(sourceFile, node) { + try { + if (!sourceFile || !node) return 1; + const lineAndChar = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); + return lineAndChar.line + 1; + } catch (error) { + return 1; // Fallback to line 1 if position cannot be determined + } +} + +/** + * Get node text with error handling + */ +function getNodeText(sourceFile, node) { + try { + if (!sourceFile || !node) return ''; + return node.getText(sourceFile); + } catch (error) { + return ''; // Fallback to empty string + } +} + +/** + * Check if a type reference uses 'any' + */ +function isAnyType(typeNode) { + return typeNode && typeNode.kind === ts.SyntaxKind.AnyKeyword; +} + +/** + * Check if node is a type reference (safe version) + */ +function isTypeReferenceSafe(node) { + return ts.isTypeReferenceNode && ts.isTypeReferenceNode(node); +} + +/** + * Check if node has JSDoc comments + */ +function hasJSDoc(node) { + return ts.getJSDocCommentsAndTags(node).length > 0; +} + +/** + * Validate TypeScript type usage + */ +function validateTypeUsage(filePath, sourceFile) { + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + + // Check for 'any' type usage - use safer checking + if (isTypeReferenceSafe(node) && isAnyType(node)) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'ANY_TYPE_USAGE', + message: 'Usage of "any" type detected', + suggestion: 'Use specific types or "unknown" for better type safety', + }); + } + + // Check for explicit any in type annotations + if (ts.isParameter(node) || ts.isVariableDeclaration(node) || ts.isPropertyDeclaration(node)) { + if (node.type && isAnyType(node.type)) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'EXPLICIT_ANY_TYPE', + message: 'Explicit "any" type annotation', + suggestion: 'Define specific interface or use "unknown" with type guards', + }); + } + } + + // Check for non-null assertion operator overuse + if (ts.isNonNullExpression(node)) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'NON_NULL_ASSERTION', + message: 'Non-null assertion operator (!) usage detected', + suggestion: 'Consider proper null checks or optional chaining instead', + }); + } + + // Check for type assertions (as Type) + if (ts.isTypeAssertion(node) || ts.isAsExpression(node)) { + const nodeText = getNodeText(sourceFile, node); + if (!nodeText.includes('unknown') && !nodeText.includes('HTMLElement') && !nodeText.includes('Element')) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'TYPE_ASSERTION', + message: 'Type assertion detected - may indicate type safety issues', + suggestion: 'Ensure type assertion is necessary and safe, consider type guards', + }); + } + } + }); +} + +/** + * Validate async/await patterns + */ +function validateAsyncPatterns(filePath, sourceFile) { + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + + // Check for missing await on Promise-returning calls + if (ts.isCallExpression(node)) { + const callText = getNodeText(sourceFile, node); + + // Check for common async methods without await + if (node.expression && ts.isPropertyAccessExpression(node.expression)) { + const methodName = node.expression.name.text; + const asyncMethods = ['initialize', 'dispose', 'save', 'load', 'fetch', 'create', 'update', 'delete']; + + if (asyncMethods.includes(methodName)) { + // Check if this call is awaited or used in Promise chain + const parent = node.parent; + const isAwaited = parent && ts.isAwaitExpression(parent); + const isInThen = callText.includes('.then(') || callText.includes('.catch('); + const isReturned = parent && ts.isReturnStatement(parent); + + if (!isAwaited && !isInThen && !isReturned) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'MISSING_AWAIT', + message: `Potential missing await on async method: ${methodName}()`, + suggestion: 'Add await keyword or handle Promise with .then()/.catch()', + }); + } + } + } + } + + // Check for async functions without try-catch + if (ts.isMethodDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isArrowFunction(node)) { + const isAsync = node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword); + + if (isAsync && node.body && ts.isBlock(node.body)) { + const hasTryCatch = node.body.statements.some(stmt => ts.isTryStatement(stmt)); + const nodeText = getNodeText(sourceFile, node); + + // Skip if function is small or has withErrorHandling wrapper + if (node.body.statements.length > 1 && !hasTryCatch && !nodeText.includes('withErrorHandling')) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'ASYNC_NO_ERROR_HANDLING', + message: 'Async function without error handling', + suggestion: 'Add try-catch block or use error handling wrapper', + }); + } + } + } + + // Check for Promise constructor usage (often indicates need for async/await) + if (ts.isNewExpression(node) && + node.expression && ts.isIdentifier(node.expression) && + node.expression.text === 'Promise') { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'PROMISE_CONSTRUCTOR', + message: 'Promise constructor usage - consider async/await pattern', + suggestion: 'Use async/await for cleaner asynchronous code', + }); + } + }); +} + +/** + * Validate error handling patterns + */ +function validateErrorHandling(filePath, sourceFile) { + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + + // Check catch clauses for proper error typing + if (ts.isCatchClause(node)) { + const catchText = getNodeText(sourceFile, node); + + // Check if error is properly typed + if (node.variableDeclaration) { + const errorParam = node.variableDeclaration; + if (!errorParam.type) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'UNTYPED_CATCH_ERROR', + message: 'Catch block parameter should be typed', + suggestion: 'Use (error: Error) or (error: unknown) for better type safety', + }); + } + } + + // Check for generic error handling + if (catchText.includes('console.log') && !catchText.includes('logger')) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'CONSOLE_LOG_IN_CATCH', + message: 'Console.log in catch block - use proper logger', + suggestion: 'Use structured logging with logger instead of console.log', + }); + } + + // Check for empty catch blocks + if (node.block.statements.length === 0) { + ERRORS.push({ + file: filePath, + line: lineNum, + type: 'EMPTY_CATCH_BLOCK', + message: 'Empty catch block - errors should be handled', + suggestion: 'Add error logging, re-throwing, or proper error handling', + }); + } + } + + // Check for throw statements with proper error types + if (ts.isThrowStatement(node) && node.expression) { + if (ts.isStringLiteral(node.expression)) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'THROW_STRING', + message: 'Throwing string instead of Error object', + suggestion: 'Throw Error objects for better stack traces and error handling', + }); + } + } + }); +} + +/** + * Validate JSDoc documentation for public APIs + */ +function validateDocumentation(filePath, sourceFile) { + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + + // Check exported functions for JSDoc + if ((ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) && + node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) { + + if (!hasJSDoc(node)) { + const functionName = node.name ? node.name.text : 'anonymous'; + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'MISSING_JSDOC', + message: `Exported function "${functionName}" missing JSDoc documentation`, + suggestion: 'Add JSDoc comments for public API documentation', + }); + } + } + + // Check exported classes for JSDoc + if (ts.isClassDeclaration(node) && + node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) { + + if (!hasJSDoc(node)) { + const className = node.name ? node.name.text : 'anonymous'; + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'MISSING_CLASS_JSDOC', + message: `Exported class "${className}" missing JSDoc documentation`, + suggestion: 'Add JSDoc comments for class documentation', + }); + } + } + + // Check interface declarations for JSDoc + if (ts.isInterfaceDeclaration(node) && + node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) { + + if (!hasJSDoc(node)) { + const interfaceName = node.name.text; + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'MISSING_INTERFACE_JSDOC', + message: `Exported interface "${interfaceName}" missing JSDoc documentation`, + suggestion: 'Add JSDoc comments for interface documentation', + }); + } + } + }); +} + +/** + * Validate generic type constraints and usage + */ +function validateGenerics(filePath, sourceFile) { + visitNode(sourceFile, sourceFile, (node, sourceFile) => { + const lineNum = getLineNumber(sourceFile, node); + + // Check type parameters for proper constraints + if (ts.isTypeParameterDeclaration && ts.isTypeParameterDeclaration(node)) { + if (!node.constraint && node.name.text === 'T') { + const parentNode = node.parent; + if (ts.isFunctionDeclaration(parentNode) || ts.isMethodDeclaration(parentNode)) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'UNCONSTRAINED_GENERIC', + message: 'Generic type T without constraints - consider adding constraints', + suggestion: 'Add constraints like > for better type safety', + }); + } + } + } + + // Check for overly complex union types + if (ts.isUnionTypeNode(node)) { + if (node.types.length > 5) { + WARNINGS.push({ + file: filePath, + line: lineNum, + type: 'COMPLEX_UNION_TYPE', + message: 'Union type with many members - consider using discriminated unions', + suggestion: 'Use discriminated unions or enums for better maintainability', + }); + } + } + }); +} + +/** + * Validate files using AST + */ +function validateFilesWithAST(filePaths) { + console.log(`šŸ” Creating TypeScript program for ${filePaths.length} files...`); + + const program = createProgram(filePaths); + + filePaths.forEach(filePath => { + const sourceFile = program.getSourceFile(filePath); + if (!sourceFile) { + console.warn(`āš ļø Could not parse ${filePath}`); + return; + } + + // Run all validations + validateTypeUsage(filePath, sourceFile); + validateAsyncPatterns(filePath, sourceFile); + validateErrorHandling(filePath, sourceFile); + validateDocumentation(filePath, sourceFile); + validateGenerics(filePath, sourceFile); + }); +} + +/** + * Find TypeScript files in all packages + */ +function findTypeScriptFiles() { + const files = []; + + function findFilesRecursive(dir, predicate) { + if (!fs.existsSync(dir)) return; + + const entries = fs.readdirSync(dir); + + for (const entry of entries) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + if (!['node_modules', 'build', 'dist', '.next', '.next-build', 'coverage', '__tests__'].includes(entry)) { + findFilesRecursive(fullPath, predicate); + } + } else if (predicate(fullPath)) { + files.push(fullPath); + } + } + } + + // Find TypeScript files in all packages + const packagesDir = path.join(process.cwd(), 'packages'); + if (fs.existsSync(packagesDir)) { + findFilesRecursive(packagesDir, (file) => + (file.endsWith('.ts') || file.endsWith('.tsx')) && + !file.endsWith('.d.ts') && + !file.includes('.test.') && + !file.includes('.spec.') + ); + } + + return files; +} + +/** + * Main validation function + */ +function validateTypeScriptBestPractices() { + console.log('šŸ” Validating TypeScript best practices (AST-based)...'); + + const files = findTypeScriptFiles(); + console.log(` Found ${files.length} TypeScript files to analyze`); + + validateFilesWithAST(files); + + // Report results + let hasErrors = false; + + if (ERRORS.length > 0) { + console.log(`\nāŒ Found ${ERRORS.length} TypeScript best practice errors:\n`); + ERRORS.forEach((error) => { + console.log(`šŸ“ ${error.file}:${error.line} [${error.type}]`); + console.log(` ${error.message}`); + console.log(` šŸ’” ${error.suggestion}\n`); + }); + hasErrors = true; + } + + if (WARNINGS.length > 0) { + console.log(`\nāš ļø Found ${WARNINGS.length} TypeScript best practice warnings:\n`); + WARNINGS.forEach((warning) => { + console.log(`šŸ“ ${warning.file}:${warning.line} [${warning.type}]`); + console.log(` ${warning.message}`); + console.log(` šŸ’” ${warning.suggestion}\n`); + }); + } + + if (!hasErrors && WARNINGS.length === 0) { + console.log('āœ… All TypeScript best practices are followed!'); + } else if (!hasErrors) { + console.log('āœ… No critical TypeScript best practice errors found!'); + console.log('šŸ’” Consider addressing warnings to improve code quality.'); + } + + // Only exit with error code for actual errors, not warnings + process.exit(hasErrors ? 1 : 0); +} + +// Run validation if called directly +if (require.main === module) { + validateTypeScriptBestPractices(); +} + +module.exports = { validateTypeScriptBestPractices }; \ No newline at end of file