@sun-asterisk/impact-analyzer 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.specify/bugs/bug-004-fix-summary.md +59 -0
- package/.specify/bugs/bug-004-raw-sql-detection.md +158 -0
- package/.specify/bugs/bug-005-queue-processor-detection.md +197 -0
- package/.specify/tasks/task-005-cli-optimization.md +284 -0
- package/.specify/tasks/task-005-completion.md +99 -0
- package/README.md +150 -36
- package/cli.js +68 -0
- package/config/default-config.js +5 -9
- package/core/detectors/database-detector.js +408 -3
- package/core/utils/logger.js +2 -1
- package/core/utils/method-call-graph.js +249 -4
- package/index.js +7 -1
- package/package.json +5 -2
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import path from 'path';
|
|
10
|
+
import fs from 'fs';
|
|
10
11
|
import { createLogger } from '../utils/logger.js';
|
|
11
12
|
|
|
12
13
|
export class DatabaseDetector {
|
|
@@ -478,6 +479,364 @@ export class DatabaseDetector {
|
|
|
478
479
|
return blocks;
|
|
479
480
|
}
|
|
480
481
|
|
|
482
|
+
/**
|
|
483
|
+
* BUG-004: Detect raw SQL query patterns in changed files
|
|
484
|
+
* CORRECTED APPROACH: Use full file content, not just diff
|
|
485
|
+
*
|
|
486
|
+
* Strategy:
|
|
487
|
+
* 1. Read full file content from filesystem
|
|
488
|
+
* 2. Find changed line numbers from diff
|
|
489
|
+
* 3. Find parent statements containing those lines
|
|
490
|
+
* 4. Extract SQL from those statements
|
|
491
|
+
*/
|
|
492
|
+
detectRawSQLQueries(filePath, diff) {
|
|
493
|
+
const sqlQueries = [];
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
// Step 1: Read full file content
|
|
497
|
+
if (!fs.existsSync(filePath)) {
|
|
498
|
+
this.logger.verbose('DatabaseDetector', `File not found: ${filePath}`);
|
|
499
|
+
return sqlQueries;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
503
|
+
const fileLines = fileContent.split('\n');
|
|
504
|
+
|
|
505
|
+
// Step 2: Extract changed line numbers from diff
|
|
506
|
+
const changedLineNumbers = this.extractChangedLineNumbers(diff);
|
|
507
|
+
|
|
508
|
+
if (changedLineNumbers.length === 0) {
|
|
509
|
+
return sqlQueries;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
this.logger.verbose('DatabaseDetector',
|
|
513
|
+
`Analyzing ${changedLineNumbers.length} changed lines in ${filePath}`);
|
|
514
|
+
|
|
515
|
+
// Step 3: Find query/execute statements that contain changed lines
|
|
516
|
+
const processedStatements = new Set();
|
|
517
|
+
|
|
518
|
+
for (const lineNum of changedLineNumbers) {
|
|
519
|
+
const statement = this.findQueryStatementContainingLine(fileLines, lineNum);
|
|
520
|
+
|
|
521
|
+
if (!statement) continue;
|
|
522
|
+
|
|
523
|
+
const key = `${statement.startLine}-${statement.endLine}`;
|
|
524
|
+
if (processedStatements.has(key)) continue;
|
|
525
|
+
processedStatements.add(key);
|
|
526
|
+
|
|
527
|
+
// Step 4: Extract SQL from statement
|
|
528
|
+
const sqlData = this.extractSQLFromStatement(statement.content);
|
|
529
|
+
|
|
530
|
+
if (sqlData) {
|
|
531
|
+
this.logger.verbose('DatabaseDetector',
|
|
532
|
+
`Found SQL query at lines ${statement.startLine}-${statement.endLine}`);
|
|
533
|
+
sqlQueries.push({
|
|
534
|
+
sql: sqlData.sql,
|
|
535
|
+
method: statement.method,
|
|
536
|
+
changedLineNum: lineNum
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
} catch (error) {
|
|
542
|
+
this.logger.error('DatabaseDetector', `Error detecting raw SQL: ${error.message}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return sqlQueries;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Extract line numbers that were changed in the diff
|
|
550
|
+
*/
|
|
551
|
+
extractChangedLineNumbers(diff) {
|
|
552
|
+
const changedLines = [];
|
|
553
|
+
const lines = diff.split('\n');
|
|
554
|
+
let currentLineNum = 0;
|
|
555
|
+
|
|
556
|
+
for (const line of lines) {
|
|
557
|
+
if (line.startsWith('@@')) {
|
|
558
|
+
// Parse: @@ -oldStart,oldCount +newStart,newCount @@
|
|
559
|
+
const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)/);
|
|
560
|
+
if (match) {
|
|
561
|
+
currentLineNum = parseInt(match[1], 10);
|
|
562
|
+
}
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
567
|
+
changedLines.push(currentLineNum);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (!line.startsWith('-')) {
|
|
571
|
+
currentLineNum++;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return changedLines;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Find the .query() or .execute() statement that contains the given line number
|
|
580
|
+
*/
|
|
581
|
+
findQueryStatementContainingLine(fileLines, targetLineNum) {
|
|
582
|
+
// Adjust for 0-based array indexing
|
|
583
|
+
const targetIndex = targetLineNum - 1;
|
|
584
|
+
|
|
585
|
+
if (targetIndex < 0 || targetIndex >= fileLines.length) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Search backwards to find .query( or .execute(
|
|
590
|
+
const searchRadius = 50;
|
|
591
|
+
let startLine = -1;
|
|
592
|
+
let method = null;
|
|
593
|
+
|
|
594
|
+
for (let i = targetIndex; i >= Math.max(0, targetIndex - searchRadius); i--) {
|
|
595
|
+
const line = fileLines[i];
|
|
596
|
+
const match = line.match(/\.(query|execute)\s*</);
|
|
597
|
+
|
|
598
|
+
if (match) {
|
|
599
|
+
method = match[1];
|
|
600
|
+
startLine = i + 1; // Convert to 1-based line number
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!method) return null;
|
|
606
|
+
|
|
607
|
+
// Search forward to find the end of the statement (closing semicolon or })
|
|
608
|
+
let endLine = targetLineNum;
|
|
609
|
+
let parenDepth = 0;
|
|
610
|
+
let inTemplate = false;
|
|
611
|
+
|
|
612
|
+
for (let i = startLine - 1; i < Math.min(fileLines.length, startLine + searchRadius); i++) {
|
|
613
|
+
const line = fileLines[i];
|
|
614
|
+
|
|
615
|
+
// Track template literal boundaries
|
|
616
|
+
for (const char of line) {
|
|
617
|
+
if (char === '`') inTemplate = !inTemplate;
|
|
618
|
+
if (!inTemplate) {
|
|
619
|
+
if (char === '(') parenDepth++;
|
|
620
|
+
if (char === ')') parenDepth--;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
endLine = i + 1;
|
|
625
|
+
|
|
626
|
+
// Found the end when parentheses are balanced and we hit a semicolon
|
|
627
|
+
if (parenDepth === 0 && line.includes(';')) {
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Extract the statement content
|
|
633
|
+
const statementLines = fileLines.slice(startLine - 1, endLine);
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
startLine,
|
|
637
|
+
endLine,
|
|
638
|
+
method,
|
|
639
|
+
content: statementLines.join('\n')
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Extract SQL and metadata from a query/execute statement
|
|
645
|
+
*/
|
|
646
|
+
extractSQLFromStatement(statementContent) {
|
|
647
|
+
// Extract SQL from template literal (backticks)
|
|
648
|
+
const templateMatch = statementContent.match(/`([^`]+)`/s);
|
|
649
|
+
|
|
650
|
+
if (templateMatch) {
|
|
651
|
+
const sql = templateMatch[1].trim();
|
|
652
|
+
return { sql };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Try single/double quotes as fallback
|
|
656
|
+
const quoteMatch = statementContent.match(/['"]([^'"]+)['"]/s);
|
|
657
|
+
if (quoteMatch) {
|
|
658
|
+
const sql = quoteMatch[1].trim();
|
|
659
|
+
return { sql };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* BUG-004: Parse SQL query to extract database impact
|
|
667
|
+
* Extracts: operation type, table names, field names
|
|
668
|
+
*/
|
|
669
|
+
parseSQLQuery(sql) {
|
|
670
|
+
const impact = {
|
|
671
|
+
operation: null,
|
|
672
|
+
tables: [],
|
|
673
|
+
fields: [],
|
|
674
|
+
hasJoin: false,
|
|
675
|
+
joinType: null
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
// Normalize SQL (remove extra whitespace, newlines)
|
|
679
|
+
const normalizedSQL = sql.replace(/\s+/g, ' ').trim().toUpperCase();
|
|
680
|
+
|
|
681
|
+
// Detect operation type
|
|
682
|
+
if (normalizedSQL.startsWith('SELECT')) {
|
|
683
|
+
impact.operation = 'SELECT';
|
|
684
|
+
} else if (normalizedSQL.startsWith('INSERT')) {
|
|
685
|
+
impact.operation = 'INSERT';
|
|
686
|
+
} else if (normalizedSQL.startsWith('UPDATE')) {
|
|
687
|
+
impact.operation = 'UPDATE';
|
|
688
|
+
} else if (normalizedSQL.startsWith('DELETE')) {
|
|
689
|
+
impact.operation = 'DELETE';
|
|
690
|
+
} else if (normalizedSQL.startsWith('ALTER')) {
|
|
691
|
+
impact.operation = 'ALTER';
|
|
692
|
+
} else if (normalizedSQL.startsWith('CREATE')) {
|
|
693
|
+
impact.operation = 'CREATE';
|
|
694
|
+
} else if (normalizedSQL.startsWith('DROP')) {
|
|
695
|
+
impact.operation = 'DROP';
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Extract table names
|
|
699
|
+
impact.tables = this.extractTablesFromSQL(normalizedSQL, impact.operation);
|
|
700
|
+
|
|
701
|
+
// Extract field names (use original SQL for case sensitivity)
|
|
702
|
+
impact.fields = this.extractFieldsFromSQL(sql, impact.operation);
|
|
703
|
+
|
|
704
|
+
// Detect JOIN
|
|
705
|
+
if (normalizedSQL.includes('JOIN')) {
|
|
706
|
+
impact.hasJoin = true;
|
|
707
|
+
|
|
708
|
+
if (normalizedSQL.includes('INNER JOIN')) {
|
|
709
|
+
impact.joinType = 'INNER';
|
|
710
|
+
} else if (normalizedSQL.includes('LEFT JOIN')) {
|
|
711
|
+
impact.joinType = 'LEFT';
|
|
712
|
+
} else if (normalizedSQL.includes('RIGHT JOIN')) {
|
|
713
|
+
impact.joinType = 'RIGHT';
|
|
714
|
+
} else if (normalizedSQL.includes('FULL JOIN')) {
|
|
715
|
+
impact.joinType = 'FULL';
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return impact;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* BUG-004: Extract table names from SQL query
|
|
724
|
+
*/
|
|
725
|
+
extractTablesFromSQL(sql, operation) {
|
|
726
|
+
const tables = new Set();
|
|
727
|
+
|
|
728
|
+
// FROM clause: SELECT ... FROM table_name
|
|
729
|
+
const fromMatches = sql.matchAll(/FROM\s+([a-z_][a-z0-9_]*)/gi);
|
|
730
|
+
for (const match of fromMatches) {
|
|
731
|
+
tables.add(match[1].toLowerCase());
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// JOIN clause: JOIN table_name
|
|
735
|
+
const joinMatches = sql.matchAll(/JOIN\s+([a-z_][a-z0-9_]*)/gi);
|
|
736
|
+
for (const match of joinMatches) {
|
|
737
|
+
tables.add(match[1].toLowerCase());
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// INSERT INTO: INSERT INTO table_name
|
|
741
|
+
if (operation === 'INSERT') {
|
|
742
|
+
const insertMatch = sql.match(/INSERT\s+INTO\s+([a-z_][a-z0-9_]*)/i);
|
|
743
|
+
if (insertMatch) {
|
|
744
|
+
tables.add(insertMatch[1].toLowerCase());
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// UPDATE: UPDATE table_name
|
|
749
|
+
if (operation === 'UPDATE') {
|
|
750
|
+
const updateMatch = sql.match(/UPDATE\s+([a-z_][a-z0-9_]*)/i);
|
|
751
|
+
if (updateMatch) {
|
|
752
|
+
tables.add(updateMatch[1].toLowerCase());
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ALTER TABLE: ALTER TABLE table_name
|
|
757
|
+
if (operation === 'ALTER') {
|
|
758
|
+
const alterMatch = sql.match(/ALTER\s+TABLE\s+([a-z_][a-z0-9_]*)/i);
|
|
759
|
+
if (alterMatch) {
|
|
760
|
+
tables.add(alterMatch[1].toLowerCase());
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return Array.from(tables);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* BUG-004: Extract field names from SQL query
|
|
769
|
+
*/
|
|
770
|
+
extractFieldsFromSQL(sql, operation) {
|
|
771
|
+
const fields = new Set();
|
|
772
|
+
|
|
773
|
+
// SELECT fields: SELECT field1, field2, table.field3
|
|
774
|
+
if (operation === 'SELECT') {
|
|
775
|
+
const selectMatch = sql.match(/SELECT\s+(.*?)\s+FROM/is);
|
|
776
|
+
if (selectMatch) {
|
|
777
|
+
const fieldList = selectMatch[1];
|
|
778
|
+
|
|
779
|
+
// Extract field names (handle table.field format)
|
|
780
|
+
const fieldMatches = fieldList.matchAll(/(?:^|,)\s*(?:\w+\.)?(\w+)(?:\s+as\s+\w+)?/gi);
|
|
781
|
+
for (const match of fieldMatches) {
|
|
782
|
+
const fieldName = match[1].toLowerCase();
|
|
783
|
+
// Skip SQL keywords and functions
|
|
784
|
+
if (!['count', 'sum', 'avg', 'max', 'min', 'distinct', '*'].includes(fieldName)) {
|
|
785
|
+
fields.add(fieldName);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// WHERE clause fields: WHERE user.id = $1 AND user.name = $2
|
|
792
|
+
const whereMatches = sql.matchAll(/(?:WHERE|AND|OR)\s+(?:\w+\.)?(\w+)\s*[=<>!]/gi);
|
|
793
|
+
for (const match of whereMatches) {
|
|
794
|
+
const fieldName = match[1].toLowerCase();
|
|
795
|
+
fields.add(fieldName);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// JOIN ON clause: ON table1.field = table2.field
|
|
799
|
+
const onMatches = sql.matchAll(/ON\s+(?:\w+\.)?(\w+)\s*=\s*(?:\w+\.)?(\w+)/gi);
|
|
800
|
+
for (const match of onMatches) {
|
|
801
|
+
fields.add(match[1].toLowerCase());
|
|
802
|
+
fields.add(match[2].toLowerCase());
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// INSERT fields: INSERT INTO table (field1, field2)
|
|
806
|
+
if (operation === 'INSERT') {
|
|
807
|
+
const insertMatch = sql.match(/INSERT\s+INTO\s+\w+\s*\((.*?)\)/is);
|
|
808
|
+
if (insertMatch) {
|
|
809
|
+
const fieldList = insertMatch[1];
|
|
810
|
+
const fieldMatches = fieldList.matchAll(/(\w+)/g);
|
|
811
|
+
for (const match of fieldMatches) {
|
|
812
|
+
fields.add(match[1].toLowerCase());
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// UPDATE SET: UPDATE table SET field1 = $1, field2 = $2
|
|
818
|
+
if (operation === 'UPDATE') {
|
|
819
|
+
const setMatch = sql.match(/SET\s+(.*?)(?:WHERE|$)/is);
|
|
820
|
+
if (setMatch) {
|
|
821
|
+
const setList = setMatch[1];
|
|
822
|
+
const fieldMatches = setList.matchAll(/(\w+)\s*=/gi);
|
|
823
|
+
for (const match of fieldMatches) {
|
|
824
|
+
fields.add(match[1].toLowerCase());
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ALTER TABLE ADD/MODIFY: ALTER TABLE table ADD COLUMN field_name
|
|
830
|
+
if (operation === 'ALTER') {
|
|
831
|
+
const alterMatches = sql.matchAll(/(?:ADD|MODIFY|DROP)\s+(?:COLUMN\s+)?(\w+)/gi);
|
|
832
|
+
for (const match of alterMatches) {
|
|
833
|
+
fields.add(match[1].toLowerCase());
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return Array.from(fields).filter(f => f !== 'delflg');
|
|
838
|
+
}
|
|
839
|
+
|
|
481
840
|
analyzeChangedCodeForDatabaseOps(changedFile, databaseChanges) {
|
|
482
841
|
const diff = changedFile.diff || '';
|
|
483
842
|
const content = changedFile.content || '';
|
|
@@ -502,6 +861,52 @@ export class DatabaseDetector {
|
|
|
502
861
|
);
|
|
503
862
|
}
|
|
504
863
|
|
|
864
|
+
// BUG-004: Step 3 - Detect raw SQL queries
|
|
865
|
+
const rawSQLQueries = this.detectRawSQLQueries(changedFile.path, changedFile.diff || '');
|
|
866
|
+
|
|
867
|
+
if (rawSQLQueries.length > 0) {
|
|
868
|
+
this.logger.verbose('DatabaseDetector',
|
|
869
|
+
`Found ${rawSQLQueries.length} raw SQL queries in ${changedFile.path}`);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
for (const query of rawSQLQueries) {
|
|
873
|
+
const sqlImpact = this.parseSQLQuery(query.sql);
|
|
874
|
+
|
|
875
|
+
// Add each affected table to database changes
|
|
876
|
+
for (const tableName of sqlImpact.tables) {
|
|
877
|
+
if (!databaseChanges.tables.has(tableName)) {
|
|
878
|
+
databaseChanges.tables.set(tableName, {
|
|
879
|
+
entity: this.tableNameToEntityName(tableName),
|
|
880
|
+
file: changedFile.path,
|
|
881
|
+
operations: new Set(),
|
|
882
|
+
fields: new Set(),
|
|
883
|
+
isEntityFile: false,
|
|
884
|
+
hasRelationChange: false,
|
|
885
|
+
changeSource: { path: changedFile.path, type: 'raw-sql' }
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const tableData = databaseChanges.tables.get(tableName);
|
|
890
|
+
|
|
891
|
+
if (sqlImpact.operation) {
|
|
892
|
+
tableData.operations.add(sqlImpact.operation);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
for (const field of sqlImpact.fields) {
|
|
896
|
+
tableData.fields.add(field);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Add SQL-specific metadata
|
|
900
|
+
if (sqlImpact.hasJoin) {
|
|
901
|
+
tableData.hasJoin = true;
|
|
902
|
+
tableData.joinType = sqlImpact.joinType;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
this.logger.verbose('DatabaseDetector',
|
|
906
|
+
`Raw SQL: ${sqlImpact.operation} on ${tableName} with fields: ${sqlImpact.fields.join(', ')}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
505
910
|
const typeOrmOps = {
|
|
506
911
|
'insert': 'INSERT',
|
|
507
912
|
'save': 'INSERT/UPDATE',
|
|
@@ -740,9 +1145,7 @@ export class DatabaseDetector {
|
|
|
740
1145
|
const methodKey = `${repoMethod.className}.${repoMethod.methodName}`;
|
|
741
1146
|
const methodCalls = this.methodCallGraph.methodCallsMap?.get(methodKey) || [];
|
|
742
1147
|
|
|
743
|
-
|
|
744
|
-
console.log(` 📞 Analyzing ${methodCalls.length} calls in ${repoMethod.className}.${repoMethod.methodName}`);
|
|
745
|
-
}
|
|
1148
|
+
this.logger.verbose('DatabaseDetector', ` 📞 Analyzing ${methodCalls.length} calls in ${repoMethod.className}.${repoMethod.methodName}`);
|
|
746
1149
|
|
|
747
1150
|
for (const call of methodCalls) {
|
|
748
1151
|
const dbOp = this.detectDatabaseOperationFromCall(call);
|
|
@@ -910,3 +1313,5 @@ export class DatabaseDetector {
|
|
|
910
1313
|
return 'low';
|
|
911
1314
|
}
|
|
912
1315
|
}
|
|
1316
|
+
|
|
1317
|
+
export default DatabaseDetector;
|
package/core/utils/logger.js
CHANGED
|
@@ -7,6 +7,7 @@ export function createLogger(verbose = false) {
|
|
|
7
7
|
return {
|
|
8
8
|
info: (msg) => console.log(msg),
|
|
9
9
|
debug: (msg) => verbose && console.log(`[DEBUG] ${msg}`),
|
|
10
|
-
verbose: (component, msg) => verbose && console.log(`[${component}] ${msg}`)
|
|
10
|
+
verbose: (component, msg) => verbose && console.log(`[${component}] ${msg}`),
|
|
11
|
+
error: (msg) => console.error(`[ERROR] ${msg}`),
|
|
11
12
|
};
|
|
12
13
|
}
|