55 * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66 */
77
8+ import * as path from 'node:path' ;
9+ import { fileURLToPath } from 'node:url' ;
810import { Messages } from '@salesforce/core' ;
911import { Common } from '../common/Common.js' ;
1012import {
@@ -32,6 +34,22 @@ type LogLevelType = (typeof LOG_LEVELS)[keyof typeof LOG_LEVELS];
3234 * Logging service with legacy formatting.
3335 */
3436export default class LoggingService {
37+ // ------------------------------------------------------//
38+ // -------------------- STATIC FIELDS ------------------ //
39+ // ------------------------------------------------------//
40+
41+ /**
42+ * Absolute plugin root path resolved from this module location.
43+ */
44+ private static readonly _PLUGIN_ROOT_PATH = path
45+ . resolve ( path . dirname ( fileURLToPath ( import . meta. url ) ) , '../../..' )
46+ . replace ( / \\ / g, '/' ) ;
47+
48+ /**
49+ * Plugin root folder name used as visible anchor in masked stack paths.
50+ */
51+ private static readonly _PLUGIN_ROOT_FOLDER = path . basename ( LoggingService . _PLUGIN_ROOT_PATH ) ;
52+
3553 // ------------------------------------------------------//
3654 // -------------------- PUBLIC FIELDS ------------------ //
3755 // ------------------------------------------------------//
@@ -519,37 +537,146 @@ export default class LoggingService {
519537 }
520538
521539 /**
522- * Masks absolute folder paths in stack traces while preserving file names and positions .
540+ * Masks stack-trace absolute paths while preserving plugin-relative paths .
523541 *
524542 * @param stack - Raw stack trace text.
525- * @returns Stack trace with masked folder paths .
543+ * @returns Stack trace with masked leading path segments .
526544 */
527545 private _maskStackTracePaths ( stack : string ) : string {
528- void this ;
529546 if ( ! stack ) {
530547 return stack ;
531548 }
532549
533- let output = stack ;
534- output = output . replace (
535- / ( f i l e : \/ \/ \/ ) (?: [ A - Z a - z ] : \/ | \/ ) ? (?: [ ^ : \r \n ( ) ] + \/ ) + ( [ ^ / : \r \n ( ) ] + ) ( : \d + (?: : \d + ) ? ) ? / g,
536- ( _full : string , prefix : string , fileName : string , position : string | undefined ) =>
537- `${ prefix } <masked-path>/${ fileName } ${ position ?? '' } `
538- ) ;
539- output = output . replace (
540- / \\ \\ [ ^ \\ \r \n ] + \\ [ ^ \\ \r \n ] + \\ (?: [ ^ \\ : \r \n ( ) ] + \\ ) + ( [ ^ \\ : \r \n ( ) ] + ) ( : \d + (?: : \d + ) ? ) ? / g,
541- ( _full : string , fileName : string , position : string | undefined ) => `<masked-path>\\${ fileName } ${ position ?? '' } `
542- ) ;
550+ const lines = stack . split ( '\n' ) . map ( ( line ) => this . _maskStackTraceLine ( line ) ) ;
551+ return lines . join ( '\n' ) ;
552+ }
553+
554+ /**
555+ * Masks absolute path tokens in one stack-trace line.
556+ *
557+ * @param line - Stack trace line.
558+ * @returns Line with masked path prefixes.
559+ */
560+ private _maskStackTraceLine ( line : string ) : string {
561+ let output = line . replace ( / \( ( [ ^ ( ) ] + ) \) / g, ( _full : string , candidate : string ) => {
562+ const masked = this . _maskStackPathCandidate ( candidate ) ;
563+ return `(${ masked } )` ;
564+ } ) ;
543565 output = output . replace (
544- / [ A - Z a - z ] : \\ (?: [ ^ \\ : \r \n ( ) ] + \\ ) + ( [ ^ \\ : \r \n ( ) ] + ) ( : \d + (?: : \d + ) ? ) ? / g ,
545- ( _full : string , fileName : string , position : string | undefined ) => `<masked-path>\\ ${ fileName } ${ position ?? '' } `
566+ / ^ \s * a t \s + ( [ ^ ( ] . + ) $ / u ,
567+ ( _full : string , candidate : string ) => ` at ${ this . _maskStackPathCandidate ( candidate ) } `
546568 ) ;
547- output = output . replace (
548- / \/ (? ! < m a s k e d - p a t h > \/ ) (?: [ ^ / : \r \n ( ) ] + \/ ) + ( [ ^ / : \r \n ( ) ] + ) ( : \d + (?: : \d + ) ? ) ? / g,
549- ( _full : string , fileName : string , position : string | undefined ) => `<masked-path>/${ fileName } ${ position ?? '' } `
569+ return output ;
570+ }
571+
572+ /**
573+ * Masks one stack path candidate and preserves plugin-relative suffix when possible.
574+ *
575+ * @param candidate - Raw stack path candidate.
576+ * @returns Masked candidate.
577+ */
578+ private _maskStackPathCandidate ( candidate : string ) : string {
579+ void this ;
580+ if ( ! candidate || candidate . includes ( '<masked-path>' ) ) {
581+ return candidate ;
582+ }
583+
584+ if ( candidate . startsWith ( 'file:///' ) ) {
585+ const rawPath = candidate . replace ( / ^ f i l e : \/ \/ \/ / u, '' ) ;
586+ const decodedPath = this . _safeDecodeUriComponent ( rawPath ) ;
587+ const { pathValue, position } = this . _splitStackPathPosition ( decodedPath ) ;
588+ const maskedPath = this . _maskAbsolutePathPrefix ( pathValue ) ;
589+ return `file:///${ maskedPath } ${ position } ` ;
590+ }
591+
592+ if ( ! this . _isAbsoluteStackPath ( candidate ) ) {
593+ return candidate ;
594+ }
595+
596+ const { pathValue, position } = this . _splitStackPathPosition ( candidate ) ;
597+ const maskedPath = this . _maskAbsolutePathPrefix ( pathValue ) ;
598+ return `${ maskedPath } ${ position } ` ;
599+ }
600+
601+ /**
602+ * Determines whether a stack candidate is an absolute filesystem path.
603+ *
604+ * @param candidate - Raw stack candidate.
605+ * @returns True when candidate is absolute path-like.
606+ */
607+ private _isAbsoluteStackPath ( candidate : string ) : boolean {
608+ void this ;
609+ return (
610+ ( candidate . length > 2 &&
611+ / [ A - Z a - z ] / u. test ( candidate [ 0 ] ) &&
612+ candidate [ 1 ] === ':' &&
613+ [ '\\' , '/' ] . includes ( candidate [ 2 ] ) ) ||
614+ candidate . startsWith ( '\\\\' ) ||
615+ candidate . startsWith ( '/' )
550616 ) ;
617+ }
551618
552- return output ;
619+ /**
620+ * Splits a path token into the path part and optional line or column suffix.
621+ *
622+ * @param value - Raw token value.
623+ * @returns Separated path and position parts.
624+ */
625+ private _splitStackPathPosition ( value : string ) : { pathValue : string ; position : string } {
626+ void this ;
627+ const match = value . match ( / ^ ( .* ?) ( : \d + (?: : \d + ) ? ) $ / u) ;
628+ if ( ! match ) {
629+ return { pathValue : value , position : '' } ;
630+ }
631+ return { pathValue : match [ 1 ] , position : match [ 2 ] } ;
632+ }
633+
634+ /**
635+ * Masks absolute prefix and keeps plugin-relative suffix when available.
636+ *
637+ * @param rawPath - Raw absolute path.
638+ * @returns Masked path.
639+ */
640+ private _maskAbsolutePathPrefix ( rawPath : string ) : string {
641+ void this ;
642+ const normalizedPath = rawPath
643+ . replace ( / \\ / g, '/' )
644+ . replace ( / ^ \/ ( [ A - Z a - z ] : \/ ) / u, '$1' )
645+ . replace ( / \/ + / g, '/' ) ;
646+ const pluginRoot = LoggingService . _PLUGIN_ROOT_PATH ;
647+ const pluginRootLower = pluginRoot . toLowerCase ( ) ;
648+ const normalizedLower = normalizedPath . toLowerCase ( ) ;
649+ const pluginRootFolder = LoggingService . _PLUGIN_ROOT_FOLDER ;
650+
651+ if ( normalizedLower === pluginRootLower || normalizedLower . startsWith ( `${ pluginRootLower } /` ) ) {
652+ const suffix = normalizedPath . slice ( pluginRoot . length ) . replace ( / ^ \/ + / u, '' ) ;
653+ return suffix ? `<masked-path>/${ suffix } ` : '<masked-path>' ;
654+ }
655+
656+ const marker = `/${ pluginRootFolder . toLowerCase ( ) } /` ;
657+ const markerIndex = normalizedLower . indexOf ( marker ) ;
658+ if ( markerIndex >= 0 ) {
659+ const relativeFromRoot = normalizedPath . slice ( markerIndex + marker . length ) ;
660+ return `<masked-path>/${ relativeFromRoot } ` ;
661+ }
662+
663+ const fileName = normalizedPath . split ( '/' ) . filter ( Boolean ) . pop ( ) ;
664+ return fileName ? `<masked-path>/${ fileName } ` : '<masked-path>' ;
665+ }
666+
667+ /**
668+ * Safely decodes URI text for file URL path masking.
669+ *
670+ * @param value - URI text.
671+ * @returns Decoded text or original value on decode errors.
672+ */
673+ private _safeDecodeUriComponent ( value : string ) : string {
674+ void this ;
675+ try {
676+ return decodeURIComponent ( value ) ;
677+ } catch {
678+ return value ;
679+ }
553680 }
554681
555682 /**
@@ -588,18 +715,10 @@ export default class LoggingService {
588715 if ( ! message ) {
589716 return '' ;
590717 }
591- let formatted : string ;
592- switch ( level ) {
593- case LOG_LEVELS . ERROR :
594- formatted = this . getMessage ( 'errorLogTemplate' , date , message ) ;
595- break ;
596- case LOG_LEVELS . WARN :
597- formatted = this . getMessage ( 'warnLogTemplate' , date , message ) ;
598- break ;
599- default :
600- formatted = this . getMessage ( 'infoLogTemplate' , date , message ) ;
601- break ;
602- }
718+ const formatted = message
719+ . split ( '\n' )
720+ . map ( ( line ) => ( line ? this . _formatStdoutLineByLevel ( level , date , line ) : '' ) )
721+ . join ( '\n' ) ;
603722
604723 if ( this . context . jsonEnabled ) {
605724 return formatted ;
@@ -613,6 +732,25 @@ export default class LoggingService {
613732 return `${ colorCode } ${ formatted } \u001b[0m` ;
614733 }
615734
735+ /**
736+ * Formats one stdout line by log level.
737+ *
738+ * @param level - Log level.
739+ * @param date - Formatted date.
740+ * @param messageLine - One message line.
741+ * @returns Formatted line.
742+ */
743+ private _formatStdoutLineByLevel ( level : LogLevelType , date : string , messageLine : string ) : string {
744+ switch ( level ) {
745+ case LOG_LEVELS . ERROR :
746+ return this . getMessage ( 'errorLogTemplate' , date , messageLine ) ;
747+ case LOG_LEVELS . WARN :
748+ return this . getMessage ( 'warnLogTemplate' , date , messageLine ) ;
749+ default :
750+ return this . getMessage ( 'infoLogTemplate' , date , messageLine ) ;
751+ }
752+ }
753+
616754 /**
617755 * Resolves the default color for a log level.
618756 *
@@ -666,13 +804,28 @@ export default class LoggingService {
666804 if ( ! message ) {
667805 return '\n' ;
668806 }
807+ return message
808+ . split ( '\n' )
809+ . map ( ( line ) => ( line ? this . _formatFileLineByLevel ( level , date , line ) : '' ) )
810+ . join ( '\n' ) ;
811+ }
812+
813+ /**
814+ * Formats one file-log line by log level.
815+ *
816+ * @param level - Log level.
817+ * @param date - Formatted date.
818+ * @param messageLine - One message line.
819+ * @returns Formatted file log line.
820+ */
821+ private _formatFileLineByLevel ( level : LogLevelType , date : string , messageLine : string ) : string {
669822 switch ( level ) {
670823 case LOG_LEVELS . ERROR :
671- return this . getMessage ( 'errorFileLogTemplate' , date , message ) ;
824+ return this . getMessage ( 'errorFileLogTemplate' , date , messageLine ) ;
672825 case LOG_LEVELS . WARN :
673- return this . getMessage ( 'warnFileLogTemplate' , date , message ) ;
826+ return this . getMessage ( 'warnFileLogTemplate' , date , messageLine ) ;
674827 default :
675- return this . getMessage ( 'infoFileLogTemplate' , date , message ) ;
828+ return this . getMessage ( 'infoFileLogTemplate' , date , messageLine ) ;
676829 }
677830 }
678831
0 commit comments